Merge branch 'develop' into wip/frizi/bazel

This commit is contained in:
Paweł Grabarz 2024-07-19 17:25:24 +02:00
commit 01b8f4ecf4
3541 changed files with 137810 additions and 85125 deletions

View File

@ -4,14 +4,16 @@ rustflags = ["--cfg", "tokio_unstable"]
[target.wasm32-unknown-unknown]
rustflags = [
# Increas the stack size from 1MB to 2MB. This is required to avoid running out of stack space
# in debug builds. The error is reported as `RuntimeError: memory access out of bounds`.
"-C",
"link-args=-z stack-size=2097152",
# Increas the stack size from 1MB to 2MB. This is required to avoid running out of stack space
# in debug builds. The error is reported as `RuntimeError: memory access out of bounds`.
"-C",
"link-args=-z stack-size=2097152",
]
[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "link-arg=/STACK:2097152"]
# Static linking is required to avoid the need for the Visual C++ Redistributable. We care about this primarily for our
# installer binary package.
rustflags = ["-C", "link-arg=/STACK:2097152", "-C", "target-feature=+crt-static"]
[target.x86_64-pc-windows-gnu]
rustflags = ["-C", "link-arg=-Wl,--stack,2097152"]

7
.envrc Normal file
View File

@ -0,0 +1,7 @@
strict_env
if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4="
fi
use flake

45
.github/CODEOWNERS vendored
View File

@ -2,50 +2,51 @@
CHANGELOG.md
# The repository settings
/.github/ @mwu-tow
/.github/ @jdunkerley @AdRiley @hubertp @Frizi
/.github/CODEOWNERS @jdunkerley @jaroslavtulach @farmaazon @AdRiley @PabloBuchu @wdanilo
# Build script & utilities
/run @mwu-tow
/run.bat @mwu-tow
/build @mwu-tow
/run @jdunkerley @AdRiley @hubertp @Frizi
/run.bat @jdunkerley @AdRiley @hubertp @Frizi
/build @jdunkerley @AdRiley @hubertp @Frizi
# Rust Libraries and Related Files
rust-toolchain.toml @vitvakatu @Frizi @kazcw @farmaazon @mwu-tow
rustfmt.toml @mwu-tow @farmaazon @vitvakatu @Frizi @kazcw
rust-toolchain.toml @vitvakatu @Frizi @kazcw @farmaazon @AdRiley
rustfmt.toml @farmaazon @vitvakatu @Frizi @kazcw @AdRiley
Cargo.lock
Cargo.toml
/lib/rust/ @mwu-tow @farmaazon @kazcw @vitvakatu @Frizi
/lib/rust/parser/ @mwu-tow @farmaazon @kazcw @vitvakatu @Frizi @jaroslavtulach
/integration-test/ @farmaazon @kazcw @vitvakatu @Frizi
/tools/build-performance/ @kazcw @mwu-tow @Akirathan
/lib/rust/ @farmaazon @kazcw @vitvakatu @Frizi @AdRiley
/lib/rust/parser/ @farmaazon @kazcw @vitvakatu @Frizi @jaroslavtulach @AdRiley
/tools/build-performance/ @kazcw @Akirathan
# Scala Libraries
/lib/scala/ @4e6 @jaroslavtulach @hubertp
# GUI
/app/gui/ @farmaazon @mwu-tow @kazcw @vitvakatu @Frizi
/app/gui/view/ @farmaazon @kazcw @vitvakatu @Frizi
/app/gui/view/graph-editor/src/builtin/visualization/java_script/ @farmaazon @kazcw @jdunkerley @vitvakatu @Frizi
/app/gui2/ @Frizi @farmaazon @vitvakatu @kazcw
/app/gui2/ @Frizi @farmaazon @vitvakatu @kazcw @AdRiley
# Engine (old)
# This section should be removed once the engine moves to /app/engine
/build.sbt @4e6 @jaroslavtulach @hubertp @Akirathan
/distribution/ @4e6 @jdunkerley @radeusgd @GregoryTravis @AdRiley
/distribution/ @4e6 @jdunkerley @radeusgd @GregoryTravis @AdRiley @marthasharkey
/engine/ @4e6 @jaroslavtulach @hubertp @Akirathan
/project/ @4e6 @jaroslavtulach @hubertp
/tools/ @4e6 @jaroslavtulach @radeusgd @hubertp
# Enso Libraries
# This section should be amended once the engine moves to /app/engine
/distribution/lib/ @jdunkerley @radeusgd @GregoryTravis @AdRiley
/std-bits/ @jdunkerley @radeusgd @GregoryTravis @AdRiley
/test/ @jdunkerley @radeusgd @GregoryTravis @AdRiley
/tools/http-test-helper/ @radeusgd @jdunkerley @GregoryTravis @AdRiley
/distribution/lib/ @jdunkerley @radeusgd @GregoryTravis @AdRiley @marthasharkey
/std-bits/ @jdunkerley @radeusgd @GregoryTravis @AdRiley @marthasharkey
/test/ @jdunkerley @radeusgd @GregoryTravis @AdRiley @marthasharkey
/tools/http-test-helper/ @radeusgd @jdunkerley @GregoryTravis @AdRiley @marthasharkey
# Dashboard, Cloud & Authentication
# Dashboard, Cloud, Authentication & Electron
/app/ide-desktop/ @PabloBuchu @indiv0 @somebody1234 @MrFlashAccount
/app/dashboard/ @PabloBuchu @indiv0 @somebody1234 @MrFlashAccount
# The data-link schema is owned by the libraries team
/app/ide-desktop/lib/dashboard/src/data/dataLinkSchema.json @radeusgd @jdunkerley @GregoryTravis @AdRiley
/app/ide-desktop/lib/dashboard/src/data/__tests__ @radeusgd @jdunkerley @GregoryTravis @AdRiley @PabloBuchu @indiv0 @somebody1234
/app/dashboard/src/data/datalinkSchema.json @radeusgd @jdunkerley @GregoryTravis @AdRiley @marthasharkey
/app/dashboard/src/data/__tests__ @radeusgd @jdunkerley @GregoryTravis @AdRiley @marthasharkey @PabloBuchu @indiv0 @somebody1234 @MrFlashAccount
# GUI / Dashboard shared
/app/*.* @Frizi @farmaazon @vitvakatu @kazcw @AdRiley @PabloBuchu @indiv0 @somebody1234 @MrFlashAccount
/app/ide-desktop/common @PabloBuchu @indiv0 @somebody1234 @MrFlashAccount @Frizi @farmaazon @vitvakatu @kazcw @AdRiley

View File

@ -21,9 +21,8 @@ Please ensure that the following checklist has been satisfied before submitting
- [ ] All code follows the
[Scala](https://github.com/enso-org/enso/blob/develop/docs/style-guide/scala.md),
[Java](https://github.com/enso-org/enso/blob/develop/docs/style-guide/java.md),
[TypeScript](https://github.com/enso-org/enso/blob/develop/docs/style-guide/typescript.md),
and
[Rust](https://github.com/enso-org/enso/blob/develop/docs/style-guide/rust.md)
style guides. In case you are using a language not listed above, follow the [Rust](https://github.com/enso-org/enso/blob/develop/docs/style-guide/rust.md) style guide.
- All code has been tested:
- [ ] Unit tests have been written where possible.
- [ ] If GUI codebase was changed, the GUI was tested when built using `./run ide build`.
- [ ] Unit tests have been written where possible.

View File

@ -107,7 +107,7 @@ jobs:
enso-build-ci-gen-job-ci-check-backend-graal-vm-ce-macos-x86_64:
name: Engine (GraalVM CE) (macos, x86_64)
runs-on:
- macos-latest
- macos-12
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
@ -270,7 +270,7 @@ jobs:
- run: ./run backend test jvm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
- if: (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository
name: Engine Test Reporter
uses: dorny/test-reporter@v1
with:
@ -327,7 +327,7 @@ jobs:
- run: ./run backend test jvm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
- if: (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository
name: Engine Test Reporter
uses: dorny/test-reporter@v1
with:
@ -354,7 +354,7 @@ jobs:
enso-build-ci-gen-job-jvm-tests-graal-vm-ce-macos-x86_64:
name: JVM Tests (GraalVM CE) (macos, x86_64)
runs-on:
- macos-latest
- macos-12
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
@ -382,7 +382,7 @@ jobs:
- run: ./run backend test jvm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
- if: (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository
name: Engine Test Reporter
uses: dorny/test-reporter@v1
with:
@ -438,7 +438,7 @@ jobs:
- run: ./run backend test jvm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
- if: (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository
name: Engine Test Reporter
uses: dorny/test-reporter@v1
with:
@ -494,7 +494,7 @@ jobs:
- run: ./run backend test jvm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
- if: (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository
name: Engine Test Reporter
uses: dorny/test-reporter@v1
with:
@ -553,7 +553,7 @@ jobs:
ENSO_LIB_S3_AWS_REGION: ${{ secrets.ENSO_LIB_S3_AWS_REGION }}
ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY: ${{ secrets.ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
- if: (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository
name: Standard Library Test Reporter
uses: dorny/test-reporter@v1
with:
@ -613,7 +613,7 @@ jobs:
ENSO_LIB_S3_AWS_REGION: ${{ secrets.ENSO_LIB_S3_AWS_REGION }}
ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY: ${{ secrets.ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
- if: (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository
name: Standard Library Test Reporter
uses: dorny/test-reporter@v1
with:
@ -640,7 +640,7 @@ jobs:
enso-build-ci-gen-job-standard-library-tests-graal-vm-ce-macos-x86_64:
name: Standard Library Tests (GraalVM CE) (macos, x86_64)
runs-on:
- macos-latest
- macos-12
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
@ -671,7 +671,7 @@ jobs:
ENSO_LIB_S3_AWS_REGION: ${{ secrets.ENSO_LIB_S3_AWS_REGION }}
ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY: ${{ secrets.ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
- if: (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository
name: Standard Library Test Reporter
uses: dorny/test-reporter@v1
with:
@ -730,7 +730,7 @@ jobs:
ENSO_LIB_S3_AWS_REGION: ${{ secrets.ENSO_LIB_S3_AWS_REGION }}
ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY: ${{ secrets.ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
- if: (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository
name: Standard Library Test Reporter
uses: dorny/test-reporter@v1
with:
@ -789,7 +789,7 @@ jobs:
ENSO_LIB_S3_AWS_REGION: ${{ secrets.ENSO_LIB_S3_AWS_REGION }}
ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY: ${{ secrets.ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
- if: (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository
name: Standard Library Test Reporter
uses: dorny/test-reporter@v1
with:

View File

@ -6,23 +6,130 @@ on:
pull_request:
branches: [develop]
paths:
- ".github/workflows/enso4igv.yml"
- "tools/enso4igv/**/*"
- "engine/**/*"
- "lib/java/**/*"
- "lib/scala/**/*"
- "build.sbt"
jobs:
build:
build_linux_parser:
runs-on: ubuntu-20.04
strategy:
matrix:
java: ["11"]
steps:
- uses: actions/checkout@v2
- name: Set up Java
uses: actions/setup-java@v2
- uses: actions/checkout@v4
- name: Install rustup
run: |
rustup target add x86_64-unknown-linux-musl
- name: Build Enso Parser
working-directory: .
env:
RUSTFLAGS: "-C target-feature=-crt-static"
run: |
cargo build --release -p enso-parser-jni -Z unstable-options --target x86_64-unknown-linux-musl --out-dir target/lib/
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
distribution: "zulu"
java-version: ${{ matrix.java }}
cache: maven
name: parser_linux
path: |
target/lib/**
build_mac_intel_parser:
runs-on: macos-12
steps:
- uses: actions/checkout@v4
- name: Build Enso Parser
working-directory: .
env:
RUSTFLAGS: "-C target-feature=-crt-static"
run: |
cargo build --release -p enso-parser-jni -Z unstable-options --out-dir target/lib/x86_64
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: parser_mac_intel
path: |
target/lib/**
build_mac_arm_parser:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Build Enso Parser
working-directory: .
env:
RUSTFLAGS: "-C target-feature=-crt-static"
run: |
cargo build --release -p enso-parser-jni -Z unstable-options --out-dir target/lib/aarch64/
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: parser_mac_arm
path: |
target/lib/**
build_windows_parser:
runs-on: windows-2019
steps:
- uses: actions/checkout@v4
- name: Build Enso Parser
working-directory: .
env:
RUSTFLAGS: "-C target-feature=-crt-static"
run: |
cargo build --release -p enso-parser-jni -Z unstable-options --out-dir target/lib/
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: parser_windows
path: |
target/lib/**
build_java:
needs:
[
build_linux_parser,
build_mac_intel_parser,
build_mac_arm_parser,
build_windows_parser,
]
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- name: Download Libraries
uses: actions/download-artifact@v4
with:
path: enso_parser
merge-multiple: true
- name: List Binaries
run: |
find . | grep -i enso.parser | xargs ls -ld
- name: Set up Rustup
run: rustup show
- uses: graalvm/setup-graalvm@v1
with:
java-version: "21"
distribution: "graalvm-community"
- name: Publish Enso Libraries to Local Maven Repository
run: sbt publishM2
- name: Find out pom & micro versions
working-directory: tools/enso4igv
@ -30,7 +137,7 @@ jobs:
# Why do we subtract a number? Read versioning policy!
# https://github.com/enso-org/enso/pull/7861#discussion_r1333133490
echo "POM_VERSION=`mvn -q -DforceStdout help:evaluate -Dexpression=project.version | cut -f1 -d -`" >> "$GITHUB_ENV"
echo "MICRO_VERSION=`expr $GITHUB_RUN_NUMBER - 2250`" >> "$GITHUB_ENV"
echo "MICRO_VERSION=`expr $GITHUB_RUN_NUMBER - 2930`" >> "$GITHUB_ENV"
- name: Update project version
working-directory: tools/enso4igv
@ -39,9 +146,10 @@ jobs:
mvn versions:set -DnewVersion="$POM_VERSION.$MICRO_VERSION"
- name: Build with Maven
run: mvn -B -Pvsix package --file tools/enso4igv/pom.xml
run: mvn -B -Pvsix package --file tools/enso4igv/pom.xml -Denso.parser.lib=`pwd`/enso_parser/
- name: Archive NBM file
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: Enso IGV Plugin
path: tools/enso4igv/target/*.nbm
@ -53,10 +161,10 @@ jobs:
run: mvn -B -Pvsix npm:exec@version --file tools/enso4igv/pom.xml
- name: Build VSCode Extension
run: mvn -B -Pvsix npm:run@vsix --file tools/enso4igv/pom.xml
run: mvn -B -Pvsix npm:run@vsix --file tools/enso4igv/pom.xml -Denso.parser.lib=`pwd`/enso_parser/
- name: Archive VSCode extension
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: VSCode Extension
path: tools/enso4igv/*.vsix

View File

@ -61,7 +61,7 @@ jobs:
enso-build-ci-gen-job-build-backend-macos-x86_64:
name: Build Backend (macos, x86_64)
runs-on:
- macos-latest
- macos-12
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
@ -193,7 +193,7 @@ jobs:
ENSO_CLOUD_COGNITO_USER_POOL_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_ID }}
ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID }}
ENSO_CLOUD_ENVIRONMENT: ${{ vars.ENSO_CLOUD_ENVIRONMENT }}
ENSO_CLOUD_REDIRECT: ${{ vars.ENSO_CLOUD_REDIRECT }}
ENSO_CLOUD_GOOGLE_ANALYTICS_TAG: ${{ vars.ENSO_CLOUD_GOOGLE_ANALYTICS_TAG }}
ENSO_CLOUD_SENTRY_DSN: ${{ vars.ENSO_CLOUD_SENTRY_DSN }}
ENSO_CLOUD_STRIPE_KEY: ${{ vars.ENSO_CLOUD_STRIPE_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -213,7 +213,7 @@ jobs:
enso-build-ci-gen-job-new-gui-build-macos-x86_64:
name: GUI build (macos, x86_64)
runs-on:
- macos-latest
- macos-12
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
@ -247,7 +247,7 @@ jobs:
ENSO_CLOUD_COGNITO_USER_POOL_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_ID }}
ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID }}
ENSO_CLOUD_ENVIRONMENT: ${{ vars.ENSO_CLOUD_ENVIRONMENT }}
ENSO_CLOUD_REDIRECT: ${{ vars.ENSO_CLOUD_REDIRECT }}
ENSO_CLOUD_GOOGLE_ANALYTICS_TAG: ${{ vars.ENSO_CLOUD_GOOGLE_ANALYTICS_TAG }}
ENSO_CLOUD_SENTRY_DSN: ${{ vars.ENSO_CLOUD_SENTRY_DSN }}
ENSO_CLOUD_STRIPE_KEY: ${{ vars.ENSO_CLOUD_STRIPE_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -302,7 +302,7 @@ jobs:
ENSO_CLOUD_COGNITO_USER_POOL_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_ID }}
ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID }}
ENSO_CLOUD_ENVIRONMENT: ${{ vars.ENSO_CLOUD_ENVIRONMENT }}
ENSO_CLOUD_REDIRECT: ${{ vars.ENSO_CLOUD_REDIRECT }}
ENSO_CLOUD_GOOGLE_ANALYTICS_TAG: ${{ vars.ENSO_CLOUD_GOOGLE_ANALYTICS_TAG }}
ENSO_CLOUD_SENTRY_DSN: ${{ vars.ENSO_CLOUD_SENTRY_DSN }}
ENSO_CLOUD_STRIPE_KEY: ${{ vars.ENSO_CLOUD_STRIPE_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -359,7 +359,7 @@ jobs:
ENSO_CLOUD_COGNITO_USER_POOL_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_ID }}
ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID }}
ENSO_CLOUD_ENVIRONMENT: ${{ vars.ENSO_CLOUD_ENVIRONMENT }}
ENSO_CLOUD_REDIRECT: ${{ vars.ENSO_CLOUD_REDIRECT }}
ENSO_CLOUD_GOOGLE_ANALYTICS_TAG: ${{ vars.ENSO_CLOUD_GOOGLE_ANALYTICS_TAG }}
ENSO_CLOUD_SENTRY_DSN: ${{ vars.ENSO_CLOUD_SENTRY_DSN }}
ENSO_CLOUD_STRIPE_KEY: ${{ vars.ENSO_CLOUD_STRIPE_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -381,7 +381,7 @@ jobs:
needs:
- enso-build-ci-gen-job-build-backend-macos-x86_64
runs-on:
- macos-latest
- macos-12
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
@ -406,12 +406,6 @@ jobs:
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: NPM install
run: npm install
- name: Uninstall old Electron Builder
run: npm uninstall --save --workspace enso electron-builder
- name: Install new Electron Builder
run: npm install --save-dev --workspace enso electron-builder@24.6.4
- run: ./run ide build --backend-source current-ci-run --gui-upload-artifact false
env:
APPLEID: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
@ -428,7 +422,7 @@ jobs:
ENSO_CLOUD_COGNITO_USER_POOL_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_ID }}
ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID }}
ENSO_CLOUD_ENVIRONMENT: ${{ vars.ENSO_CLOUD_ENVIRONMENT }}
ENSO_CLOUD_REDIRECT: ${{ vars.ENSO_CLOUD_REDIRECT }}
ENSO_CLOUD_GOOGLE_ANALYTICS_TAG: ${{ vars.ENSO_CLOUD_GOOGLE_ANALYTICS_TAG }}
ENSO_CLOUD_SENTRY_DSN: ${{ vars.ENSO_CLOUD_SENTRY_DSN }}
ENSO_CLOUD_STRIPE_KEY: ${{ vars.ENSO_CLOUD_STRIPE_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -485,7 +479,7 @@ jobs:
ENSO_CLOUD_COGNITO_USER_POOL_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_ID }}
ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID }}
ENSO_CLOUD_ENVIRONMENT: ${{ vars.ENSO_CLOUD_ENVIRONMENT }}
ENSO_CLOUD_REDIRECT: ${{ vars.ENSO_CLOUD_REDIRECT }}
ENSO_CLOUD_GOOGLE_ANALYTICS_TAG: ${{ vars.ENSO_CLOUD_GOOGLE_ANALYTICS_TAG }}
ENSO_CLOUD_SENTRY_DSN: ${{ vars.ENSO_CLOUD_SENTRY_DSN }}
ENSO_CLOUD_STRIPE_KEY: ${{ vars.ENSO_CLOUD_STRIPE_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,907 +0,0 @@
# This file is auto-generated. Do not edit it manually!
# Edit the enso_build::ci_gen module instead and run `cargo run --package enso-build-ci-gen`.
name: Engine Nightly Checks
on:
schedule:
- cron: 0 3 * * *
workflow_dispatch:
inputs:
clean_build_required:
description: Clean before and after the run.
required: false
type: boolean
default: false
jobs:
enso-build-ci-gen-job-ci-check-backend-graal-vm-ce-linux-x86_64:
name: Engine (GraalVM CE) (linux, x86_64)
runs-on:
- self-hosted
- Linux
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.2.1
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v7
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- name: Checking out the repository
uses: actions/checkout@v4
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help || (git clean -ffdx && ./run --help)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: "(contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: ./run backend ci-check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "(always()) && (contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
env:
GRAAL_EDITION: GraalVM CE
enso-build-ci-gen-job-ci-check-backend-graal-vm-ce-macos-aarch64:
name: Engine (GraalVM CE) (macos, aarch64)
runs-on:
- self-hosted
- macOS
- ARM64
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.2.1
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v7
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- name: Checking out the repository
uses: actions/checkout@v4
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help || (git clean -ffdx && ./run --help)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: "(contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: ./run backend ci-check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "(always()) && (contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
env:
GRAAL_EDITION: GraalVM CE
enso-build-ci-gen-job-ci-check-backend-graal-vm-ce-macos-x86_64:
name: Engine (GraalVM CE) (macos, x86_64)
runs-on:
- macos-latest
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.2.1
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v7
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- name: Checking out the repository
uses: actions/checkout@v4
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help || (git clean -ffdx && ./run --help)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: "(contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: ./run backend ci-check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "(always()) && (contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
env:
GRAAL_EDITION: GraalVM CE
enso-build-ci-gen-job-ci-check-backend-graal-vm-ce-windows-x86_64:
name: Engine (GraalVM CE) (windows, x86_64)
runs-on:
- self-hosted
- Windows
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.2.1
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v7
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- name: Checking out the repository
uses: actions/checkout@v4
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help || (git clean -ffdx && ./run --help)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: "(contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: ./run backend ci-check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "(always()) && (contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
env:
GRAAL_EDITION: GraalVM CE
enso-build-ci-gen-job-ci-check-backend-oracle-graal-vm-linux-x86_64:
name: Engine (Oracle GraalVM) (linux, x86_64)
runs-on:
- self-hosted
- Linux
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.2.1
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v7
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- name: Checking out the repository
uses: actions/checkout@v4
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help || (git clean -ffdx && ./run --help)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: "(contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: ./run backend ci-check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "(always()) && (contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
env:
GRAAL_EDITION: Oracle GraalVM
enso-build-ci-gen-job-jvm-tests-graal-vm-ce-linux-x86_64:
name: JVM Tests (GraalVM CE) (linux, x86_64)
runs-on:
- self-hosted
- Linux
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.2.1
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v7
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- name: Checking out the repository
uses: actions/checkout@v4
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help || (git clean -ffdx && ./run --help)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: "(contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: ./run backend test jvm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
name: Engine Test Reporter
uses: dorny/test-reporter@v1
with:
max-annotations: 50
name: Engine Tests Report (GraalVM CE, linux, x86_64)
path: ${{ env.ENSO_TEST_JUNIT_DIR }}/*.xml
path-replace-backslashes: true
reporter: java-junit
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "(always()) && (contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
env:
GRAAL_EDITION: GraalVM CE
permissions:
checks: write
enso-build-ci-gen-job-jvm-tests-graal-vm-ce-macos-aarch64:
name: JVM Tests (GraalVM CE) (macos, aarch64)
runs-on:
- self-hosted
- macOS
- ARM64
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.2.1
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v7
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- name: Checking out the repository
uses: actions/checkout@v4
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help || (git clean -ffdx && ./run --help)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: "(contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: ./run backend test jvm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
name: Engine Test Reporter
uses: dorny/test-reporter@v1
with:
max-annotations: 50
name: Engine Tests Report (GraalVM CE, macos, aarch64)
path: ${{ env.ENSO_TEST_JUNIT_DIR }}/*.xml
path-replace-backslashes: true
reporter: java-junit
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "(always()) && (contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
env:
GRAAL_EDITION: GraalVM CE
permissions:
checks: write
enso-build-ci-gen-job-jvm-tests-graal-vm-ce-macos-x86_64:
name: JVM Tests (GraalVM CE) (macos, x86_64)
runs-on:
- macos-latest
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.2.1
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v7
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- name: Checking out the repository
uses: actions/checkout@v4
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help || (git clean -ffdx && ./run --help)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: "(contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: ./run backend test jvm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
name: Engine Test Reporter
uses: dorny/test-reporter@v1
with:
max-annotations: 50
name: Engine Tests Report (GraalVM CE, macos, x86_64)
path: ${{ env.ENSO_TEST_JUNIT_DIR }}/*.xml
path-replace-backslashes: true
reporter: java-junit
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "(always()) && (contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
env:
GRAAL_EDITION: GraalVM CE
permissions:
checks: write
enso-build-ci-gen-job-jvm-tests-graal-vm-ce-windows-x86_64:
name: JVM Tests (GraalVM CE) (windows, x86_64)
runs-on:
- self-hosted
- Windows
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.2.1
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v7
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- name: Checking out the repository
uses: actions/checkout@v4
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help || (git clean -ffdx && ./run --help)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: "(contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: ./run backend test jvm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
name: Engine Test Reporter
uses: dorny/test-reporter@v1
with:
max-annotations: 50
name: Engine Tests Report (GraalVM CE, windows, x86_64)
path: ${{ env.ENSO_TEST_JUNIT_DIR }}/*.xml
path-replace-backslashes: true
reporter: java-junit
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "(always()) && (contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
env:
GRAAL_EDITION: GraalVM CE
permissions:
checks: write
enso-build-ci-gen-job-jvm-tests-oracle-graal-vm-linux-x86_64:
name: JVM Tests (Oracle GraalVM) (linux, x86_64)
runs-on:
- self-hosted
- Linux
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.2.1
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v7
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- name: Checking out the repository
uses: actions/checkout@v4
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help || (git clean -ffdx && ./run --help)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: "(contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: ./run backend test jvm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
name: Engine Test Reporter
uses: dorny/test-reporter@v1
with:
max-annotations: 50
name: Engine Tests Report (Oracle GraalVM, linux, x86_64)
path: ${{ env.ENSO_TEST_JUNIT_DIR }}/*.xml
path-replace-backslashes: true
reporter: java-junit
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "(always()) && (contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
env:
GRAAL_EDITION: Oracle GraalVM
permissions:
checks: write
enso-build-ci-gen-job-standard-library-tests-graal-vm-ce-linux-x86_64:
name: Standard Library Tests (GraalVM CE) (linux, x86_64)
runs-on:
- self-hosted
- Linux
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.2.1
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v7
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- name: Checking out the repository
uses: actions/checkout@v4
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help || (git clean -ffdx && ./run --help)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: "(contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: ./run backend test standard-library
env:
ENSO_LIB_S3_AWS_ACCESS_KEY_ID: ${{ secrets.ENSO_LIB_S3_AWS_ACCESS_KEY_ID }}
ENSO_LIB_S3_AWS_REGION: ${{ secrets.ENSO_LIB_S3_AWS_REGION }}
ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY: ${{ secrets.ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
name: Standard Library Test Reporter
uses: dorny/test-reporter@v1
with:
max-annotations: 50
name: Standard Library Tests Report (GraalVM CE, linux, x86_64)
path: ${{ env.ENSO_TEST_JUNIT_DIR }}/*/*.xml
path-replace-backslashes: true
reporter: java-junit
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "(always()) && (contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
env:
GRAAL_EDITION: GraalVM CE
permissions:
checks: write
enso-build-ci-gen-job-standard-library-tests-graal-vm-ce-macos-aarch64:
name: Standard Library Tests (GraalVM CE) (macos, aarch64)
runs-on:
- self-hosted
- macOS
- ARM64
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.2.1
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v7
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- name: Checking out the repository
uses: actions/checkout@v4
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help || (git clean -ffdx && ./run --help)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: "(contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: ./run backend test standard-library
env:
ENSO_LIB_S3_AWS_ACCESS_KEY_ID: ${{ secrets.ENSO_LIB_S3_AWS_ACCESS_KEY_ID }}
ENSO_LIB_S3_AWS_REGION: ${{ secrets.ENSO_LIB_S3_AWS_REGION }}
ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY: ${{ secrets.ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
name: Standard Library Test Reporter
uses: dorny/test-reporter@v1
with:
max-annotations: 50
name: Standard Library Tests Report (GraalVM CE, macos, aarch64)
path: ${{ env.ENSO_TEST_JUNIT_DIR }}/*/*.xml
path-replace-backslashes: true
reporter: java-junit
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "(always()) && (contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
env:
GRAAL_EDITION: GraalVM CE
permissions:
checks: write
enso-build-ci-gen-job-standard-library-tests-graal-vm-ce-macos-x86_64:
name: Standard Library Tests (GraalVM CE) (macos, x86_64)
runs-on:
- macos-latest
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.2.1
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v7
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- name: Checking out the repository
uses: actions/checkout@v4
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help || (git clean -ffdx && ./run --help)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: "(contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: ./run backend test standard-library
env:
ENSO_LIB_S3_AWS_ACCESS_KEY_ID: ${{ secrets.ENSO_LIB_S3_AWS_ACCESS_KEY_ID }}
ENSO_LIB_S3_AWS_REGION: ${{ secrets.ENSO_LIB_S3_AWS_REGION }}
ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY: ${{ secrets.ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
name: Standard Library Test Reporter
uses: dorny/test-reporter@v1
with:
max-annotations: 50
name: Standard Library Tests Report (GraalVM CE, macos, x86_64)
path: ${{ env.ENSO_TEST_JUNIT_DIR }}/*/*.xml
path-replace-backslashes: true
reporter: java-junit
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "(always()) && (contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
env:
GRAAL_EDITION: GraalVM CE
permissions:
checks: write
enso-build-ci-gen-job-standard-library-tests-graal-vm-ce-windows-x86_64:
name: Standard Library Tests (GraalVM CE) (windows, x86_64)
runs-on:
- self-hosted
- Windows
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.2.1
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v7
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- name: Checking out the repository
uses: actions/checkout@v4
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help || (git clean -ffdx && ./run --help)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: "(contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: ./run backend test standard-library
env:
ENSO_LIB_S3_AWS_ACCESS_KEY_ID: ${{ secrets.ENSO_LIB_S3_AWS_ACCESS_KEY_ID }}
ENSO_LIB_S3_AWS_REGION: ${{ secrets.ENSO_LIB_S3_AWS_REGION }}
ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY: ${{ secrets.ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
name: Standard Library Test Reporter
uses: dorny/test-reporter@v1
with:
max-annotations: 50
name: Standard Library Tests Report (GraalVM CE, windows, x86_64)
path: ${{ env.ENSO_TEST_JUNIT_DIR }}/*/*.xml
path-replace-backslashes: true
reporter: java-junit
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "(always()) && (contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
env:
GRAAL_EDITION: GraalVM CE
permissions:
checks: write
enso-build-ci-gen-job-standard-library-tests-oracle-graal-vm-linux-x86_64:
name: Standard Library Tests (Oracle GraalVM) (linux, x86_64)
runs-on:
- self-hosted
- Linux
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.2.1
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v7
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- name: Checking out the repository
uses: actions/checkout@v4
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help || (git clean -ffdx && ./run --help)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: "(contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: ./run backend test standard-library
env:
ENSO_LIB_S3_AWS_ACCESS_KEY_ID: ${{ secrets.ENSO_LIB_S3_AWS_ACCESS_KEY_ID }}
ENSO_LIB_S3_AWS_REGION: ${{ secrets.ENSO_LIB_S3_AWS_REGION }}
ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY: ${{ secrets.ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
name: Standard Library Test Reporter
uses: dorny/test-reporter@v1
with:
max-annotations: 50
name: Standard Library Tests Report (Oracle GraalVM, linux, x86_64)
path: ${{ env.ENSO_TEST_JUNIT_DIR }}/*/*.xml
path-replace-backslashes: true
reporter: java-junit
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "(always()) && (contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
env:
GRAAL_EDITION: Oracle GraalVM
permissions:
checks: write
env:
ENSO_BUILD_SKIP_VERSION_CHECK: "true"

View File

@ -202,7 +202,7 @@ jobs:
needs:
- enso-build-ci-gen-draft-release-linux-x86_64
runs-on:
- macos-latest
- macos-12
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
@ -389,7 +389,7 @@ jobs:
ENSO_CLOUD_COGNITO_USER_POOL_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_ID }}
ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID }}
ENSO_CLOUD_ENVIRONMENT: ${{ vars.ENSO_CLOUD_ENVIRONMENT }}
ENSO_CLOUD_REDIRECT: ${{ vars.ENSO_CLOUD_REDIRECT }}
ENSO_CLOUD_GOOGLE_ANALYTICS_TAG: ${{ vars.ENSO_CLOUD_GOOGLE_ANALYTICS_TAG }}
ENSO_CLOUD_SENTRY_DSN: ${{ vars.ENSO_CLOUD_SENTRY_DSN }}
ENSO_CLOUD_STRIPE_KEY: ${{ vars.ENSO_CLOUD_STRIPE_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -442,12 +442,6 @@ jobs:
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: NPM install
run: npm install
- name: Uninstall old Electron Builder
run: npm uninstall --save --workspace enso electron-builder
- name: Install new Electron Builder
run: npm install --save-dev --workspace enso electron-builder@24.6.4
- run: ./run ide upload --backend-source release --backend-release ${{env.ENSO_RELEASE_ID}}
env:
APPLEID: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
@ -464,7 +458,7 @@ jobs:
ENSO_CLOUD_COGNITO_USER_POOL_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_ID }}
ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID }}
ENSO_CLOUD_ENVIRONMENT: ${{ vars.ENSO_CLOUD_ENVIRONMENT }}
ENSO_CLOUD_REDIRECT: ${{ vars.ENSO_CLOUD_REDIRECT }}
ENSO_CLOUD_GOOGLE_ANALYTICS_TAG: ${{ vars.ENSO_CLOUD_GOOGLE_ANALYTICS_TAG }}
ENSO_CLOUD_SENTRY_DSN: ${{ vars.ENSO_CLOUD_SENTRY_DSN }}
ENSO_CLOUD_STRIPE_KEY: ${{ vars.ENSO_CLOUD_STRIPE_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -490,7 +484,7 @@ jobs:
- enso-build-ci-gen-draft-release-linux-x86_64
- enso-build-ci-gen-job-upload-backend-macos-x86_64
runs-on:
- macos-latest
- macos-12
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
@ -515,12 +509,6 @@ jobs:
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: NPM install
run: npm install
- name: Uninstall old Electron Builder
run: npm uninstall --save --workspace enso electron-builder
- name: Install new Electron Builder
run: npm install --save-dev --workspace enso electron-builder@24.6.4
- run: ./run ide upload --backend-source release --backend-release ${{env.ENSO_RELEASE_ID}}
env:
APPLEID: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
@ -537,7 +525,7 @@ jobs:
ENSO_CLOUD_COGNITO_USER_POOL_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_ID }}
ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID }}
ENSO_CLOUD_ENVIRONMENT: ${{ vars.ENSO_CLOUD_ENVIRONMENT }}
ENSO_CLOUD_REDIRECT: ${{ vars.ENSO_CLOUD_REDIRECT }}
ENSO_CLOUD_GOOGLE_ANALYTICS_TAG: ${{ vars.ENSO_CLOUD_GOOGLE_ANALYTICS_TAG }}
ENSO_CLOUD_SENTRY_DSN: ${{ vars.ENSO_CLOUD_SENTRY_DSN }}
ENSO_CLOUD_STRIPE_KEY: ${{ vars.ENSO_CLOUD_STRIPE_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -598,7 +586,7 @@ jobs:
ENSO_CLOUD_COGNITO_USER_POOL_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_ID }}
ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID: ${{ vars.ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID }}
ENSO_CLOUD_ENVIRONMENT: ${{ vars.ENSO_CLOUD_ENVIRONMENT }}
ENSO_CLOUD_REDIRECT: ${{ vars.ENSO_CLOUD_REDIRECT }}
ENSO_CLOUD_GOOGLE_ANALYTICS_TAG: ${{ vars.ENSO_CLOUD_GOOGLE_ANALYTICS_TAG }}
ENSO_CLOUD_SENTRY_DSN: ${{ vars.ENSO_CLOUD_SENTRY_DSN }}
ENSO_CLOUD_STRIPE_KEY: ${{ vars.ENSO_CLOUD_STRIPE_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -75,7 +75,7 @@ jobs:
enso-build-ci-gen-job-ci-check-backend-graal-vm-ce-macos-x86_64:
name: Engine (GraalVM CE) (macos, x86_64)
runs-on:
- macos-latest
- macos-12
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
@ -193,7 +193,7 @@ jobs:
- run: ./run backend test jvm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
- if: (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository
name: Engine Test Reporter
uses: dorny/test-reporter@v1
with:
@ -220,7 +220,7 @@ jobs:
enso-build-ci-gen-job-jvm-tests-graal-vm-ce-macos-x86_64:
name: JVM Tests (GraalVM CE) (macos, x86_64)
runs-on:
- macos-latest
- macos-12
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
@ -248,7 +248,7 @@ jobs:
- run: ./run backend test jvm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
- if: (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository
name: Engine Test Reporter
uses: dorny/test-reporter@v1
with:
@ -304,7 +304,7 @@ jobs:
- run: ./run backend test jvm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
- if: (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository
name: Engine Test Reporter
uses: dorny/test-reporter@v1
with:
@ -363,7 +363,7 @@ jobs:
ENSO_LIB_S3_AWS_REGION: ${{ secrets.ENSO_LIB_S3_AWS_REGION }}
ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY: ${{ secrets.ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
- if: (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository
name: Standard Library Test Reporter
uses: dorny/test-reporter@v1
with:
@ -390,7 +390,7 @@ jobs:
enso-build-ci-gen-job-standard-library-tests-graal-vm-ce-macos-x86_64:
name: Standard Library Tests (GraalVM CE) (macos, x86_64)
runs-on:
- macos-latest
- macos-12
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
@ -421,7 +421,7 @@ jobs:
ENSO_LIB_S3_AWS_REGION: ${{ secrets.ENSO_LIB_S3_AWS_REGION }}
ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY: ${{ secrets.ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
- if: (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository
name: Standard Library Test Reporter
uses: dorny/test-reporter@v1
with:
@ -480,7 +480,7 @@ jobs:
ENSO_LIB_S3_AWS_REGION: ${{ secrets.ENSO_LIB_S3_AWS_REGION }}
ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY: ${{ secrets.ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: success() || failure()
- if: (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository
name: Standard Library Test Reporter
uses: dorny/test-reporter@v1
with:

20
.gitignore vendored
View File

@ -118,6 +118,11 @@ project/metals.sbt
/app/ide-desktop/lib/client/electron-builder-config.json
*.config.ts.timestamp-*
# Resources fire generated build-time for Win installer/uninstaller.
/build/install/installer/archive.rc
/build/install/icon.rc
#################
## Build Cache ##
#################
@ -133,7 +138,6 @@ build-cache
##################
*.build_artifacts.txt
/runner
######################
## Enso-Development ##
@ -166,3 +170,17 @@ test-results
*.ir
*.meta
.enso/
##################
## direnv cache ##
##################
.direnv
##########################
## Playwright artifacts ##
##########################
test-results/
playwright-report/
playwright/.cache/

View File

@ -29,6 +29,7 @@ tools/http-test-helper/www-files
**/scala-parser.js
**/package-lock.json
**/msdfgen_wasm.js
pnpm-lock.yaml
# Generated files
app/ide-desktop/lib/client/electron-builder-config.json

7
.vscode/launch.json vendored
View File

@ -27,6 +27,13 @@
"command": "npm run dev",
// "env": {"NODE_OPTIONS": "--inspect"},
"cwd": "${workspaceFolder}/app/gui2"
},
{
"type": "nativeimage",
"request": "launch",
"name": "Launch Native Image",
"nativeImagePath": "${workspaceFolder}/runner",
"args": "--run ${file}"
}
]
}

View File

@ -24,5 +24,7 @@
"files.watcherExclude": {
"**/target": true
},
"vitest.workspaceConfig": "vitest.workspace.ts"
"vitest.workspaceConfig": "vitest.workspace.ts",
"metals.inlayHints.implicitArguments.enable": true,
"metals.inlayHints.implicitConversions.enable": true
}

745
CHANGELOG-2021.md Normal file
View File

@ -0,0 +1,745 @@
# Enso 2.0.0-alpha.18 (2021-10-12)
<br/>![New Features](/docs/assets/tags/new_features.svg)
#### Enso Compiler
- [Updated Enso engine to version 0.2.30][engine-0.2.31]. If you're interested
in the enhancements and fixes made to the Enso compiler, you can find their
release notes
[here](https://github.com/enso-org/enso/blob/develop/RELEASES.md).
<br/>![Bug Fixes](/docs/assets/tags/bug_fixes.svg)
#### Visual Environment
- [Fixed freezing after inactivity.][1776] When the IDE window was minimized or
covered by other windows or invisible for any other reason for a duration
around one minute or longer then it would often be frozen for some seconds on
return. Now it is possible to interact with the IDE instantly, no matter how
long it had been inactive.
<br/>
[1776]: https://github.com/enso-org/ide/pull/1776
# Enso 2.0.0-alpha.17 (2021-09-23)
<br/>![Bug Fixes](/docs/assets/tags/bug_fixes.svg)
#### Visual Environment
- [Correct handling of command-line flags.][1815] Command line arguments of the
form `--backend=false` or `--backend false` are now handled as expected and
turn off the "backend" option. The same fix has been applied to all other
boolean command-line options as well.
- [Visualizations will be attached after project is ready.][1825] This addresses
a rare issue when initially opened visualizations were automatically closed
rather than filled with data.
<br/>
[1815]: https://github.com/enso-org/ide/pull/1815
[1825]: https://github.com/enso-org/ide/pull/1825
<br/>![New Features](/docs/assets/tags/new_features.svg)
#### Enso Compiler
- [Updated Enso engine to version 0.2.30][engine-0.2.30]. If you're interested
in the enhancements and fixes made to the Enso compiler, you can find their
release notes
[here](https://github.com/enso-org/enso/blob/develop/RELEASES.md).
[engine-0.2.30]: https://github.com/enso-org/enso/blob/develop/RELEASES.md
# Enso 2.0.0-alpha.16 (2021-09-16)
<br/>![New Features](/docs/assets/tags/new_features.svg)
#### Visual Environment
- [Auto-layout for new nodes.][1755] When a node is selected and a new node gets
created below using <kbd>Tab</kbd> then the new node is automatically
positioned far enough to the right to find sufficient space and avoid
overlapping with existing nodes.
[1755]: https://github.com/enso-org/ide/pull/1755
#### Enso Compiler
- [Updated Enso engine to version 0.2.29][engine-0.2.29]. If you're interested
in the enhancements and fixes made to the Enso compiler, you can find their
release notes
[here](https://github.com/enso-org/enso/blob/develop/RELEASES.md).
[engine-0.2.29]: https://github.com/enso-org/enso/blob/develop/RELEASES.md
<br/>![Bug Fixes](/docs/assets/tags/bug_fixes.svg)
#### Visual Environment
- [Sharp rendering on screens with fractional pixel ratios.][1820]
[1820]: https://github.com/enso-org/ide/pull/1820
<br/>
# Enso 2.0.0-alpha.15 (2021-09-09)
<br/>![Bug Fixes](/docs/assets/tags/bug_fixes.svg)
#### Visual Environment
- [Fixed parsing of the `--no-data-gathering` command line option.][1831] Flag's
name has been changed to `--data-gathering`, so now `--data-gathering=false`
and `--data-gathering=true` are supported as well.
[1831]: https://github.com/enso-org/ide/pull/1831
# Enso 2.0.0-alpha.14 (2021-09-02)
<br/>![New Features](/docs/assets/tags/new_features.svg)
#### Visual Environment
- [Visualization previews are disabled.][1817] Previously, hovering over a
node's output port for more than four seconds would temporarily reveal the
node's visualization. This behavior is disabled now.
[1817]: https://github.com/enso-org/ide/pull/1817
#### Enso Compiler
- [Updated Enso engine to version 0.2.28][1829]. If you're interested in the
enhancements and fixes made to the Enso compiler, you can find their release
notes [here](https://github.com/enso-org/enso/blob/develop/RELEASES.md).
[1829]: https://github.com/enso-org/ide/pull/1829
# Enso 2.0.0-alpha.13 (2021-08-27)
<br/>![New Features](/docs/assets/tags/new_features.svg)
#### Enso Compiler
- [Updated Enso engine to version 0.2.27][1811]. If you're interested in the
enhancements and fixes made to the Enso compiler, you can find their release
notes [here](https://github.com/enso-org/enso/blob/develop/RELEASES.md).
[1811]: https://github.com/enso-org/ide/pull/1811
# Enso 2.0.0-alpha.12 (2021-08-13)
<br/>![New Features](/docs/assets/tags/new_features.svg)
#### Visual Environment
- [Improvements to visualization handling][1804]. These improvements are fixing
possible performance issues around attaching and detaching visualizations.
- [GeoMap visualization will ignore points with `null` coordinates][1775]. Now
the presence of such points in the dataset will not break initial map
positioning.
#### Enso Compiler
- [Updated Enso engine to version 0.2.26][1801]. If you're interested in the
enhancements and fixes made to the Enso compiler, you can find their release
notes [here](https://github.com/enso-org/enso/blob/develop/RELEASES.md).
[1801]: https://github.com/enso-org/ide/pull/1801
[1775]: https://github.com/enso-org/ide/pull/1775
[1798]: https://github.com/enso-org/ide/pull/1798
[1804]: https://github.com/enso-org/ide/pull/1804
# Enso 2.0.0-alpha.11 (2021-08-09)
This update contains major performance improvements and exposes new privacy user
settings. We will work towards stabilizing it in the next weeks in order to make
these updates be shipped in a stable release before the end of the year.
<br/>![New Features](/docs/assets/tags/new_features.svg)
#### Visual Environment
- [New look of open project dialog][1700]. Now it has a "Open project" title at
the top.
- [Documentation coments are displayed next to the nodes.][1744].
#### Enso Compiler
- [Updated Enso engine to version 0.2.22][1762]. If you are interested in the
enhancements and fixes made to the Enso compiler, you can find out more
details in
[the engine release notes](https://github.com/enso-org/enso/blob/develop/RELEASES.md).
<br/>![Bug Fixes](/docs/assets/tags/bug_fixes.svg)
#### Visual Environment
- [Fixed a bug where edited node expression was sometimes altered.][1743] When
editing node expression, the changes were occasionally reverted, or the
grayed-out parameter names were added to the actual expression. <br/>
[1700]: https://github.com/enso-org/ide/pull/1700
[1742]: https://github.com/enso-org/ide/pull/1742
[1726]: https://github.com/enso-org/ide/pull/1762
[1743]: https://github.com/enso-org/ide/pull/1743
[1744]: https://github.com/enso-org/ide/pull/1744
# Enso 2.0.0-alpha.10 (2021-07-23)
<br/>![New Features](/docs/assets/tags/new_features.svg)
#### Enso Compiler
- [Updated Enso engine to version 0.2.15][1710]. If you're interested in the
enhancements and fixes made to the Enso compiler, you can find out more
details in
[the engine release notes](https://github.com/enso-org/enso/blob/develop/RELEASES.md).
<br/>
[1710]: https://github.com/enso-org/ide/pull/1710
# Enso 2.0.0-alpha.9 (2021-07-16)
<br/>![New Features](/docs/assets/tags/new_features.svg)
#### Visual Environment
- [Improved undo-redo][1653]. Node selection, enabling/disabling visualisations
and entering a node are now affected by undo/redo and are restored on project
startup.
<br/>
[1640]: https://github.com/enso-org/ide/pull/1653
# Enso 2.0.0-alpha.8 (2021-06-09)
<br/>![New Features](/docs/assets/tags/new_features.svg)
#### Enso Compiler
- [Updated Enso engine to version 0.2.12][1640]. If you're interested in the
enhancements and fixes made to the Enso compiler, you can find out more
details in
[the engine release notes](https://github.com/enso-org/enso/blob/develop/RELEASES.md).
[1640]: https://github.com/enso-org/ide/pull/1640
<br/>
# Enso 2.0.0-alpha.7 (2021-06-06)
<br/>![New Features](/docs/assets/tags/new_features.svg)
#### Visual Environment
- [User Authentication][1653]. Users can sign in to Enso using Google, GitHub or
email accounts.
<br/>![Bug Fixes](/docs/assets/tags/bug_fixes.svg)
#### Visual Environment
- [Fix node selection bug ][1664]. Fix nodes not being deselected correctly in
some circumstances. This would lead to nodes moving too fast when dragged
[1650] or the internal state of the project being inconsistent [1626].
[1653]: https://github.com/enso-org/ide/pull/1653
[1664]: https://github.com/enso-org/ide/pull/1664
<br/>
# Enso 2.0.0-alpha.6 (2021-06-28)
<br/>![New Features](/docs/assets/tags/new_features.svg)
#### Visual Environment
- [Profling mode.][1546] The IDE contains a profiling mode now which can be
entered through a button in the top-right corner or through the keybinding
<kbd>ctrl</kbd>+<kbd>p</kbd>. This mode does not display any information yet.
In the future, it will display the running times of nodes and maybe more
useful statistics.
- [Area selection][1588]. You can now select multiple nodes at once. Just click
and drag on the background of your graph and see the beauty of the area
selection appear.
- [Opening projects in application graphical interface][1587]. Press `cmd`+`o`
to bring the list of projects. Select a project on the list to open it.
- [Initial support for undo-redo][1602]. Press <kbd>cmd</kbd>+<kbd>z</kbd> to
undo last action and <kbd>cmd</kbd>+<kbd>z</kbd> to redo last undone action.
This version of undo redo does not have proper support for text editor and
undoing UI changes (like selecting nodes).
#### EnsoGL (rendering engine)
<br/>![Bug Fixes](/docs/assets/tags/bug_fixes.svg)
#### Visual Environment
- [Nodes in graph no longer overlap panels][1577]. The Searcher, project name,
breadcrumbs and status bar are displayed "above" nodes.
#### Enso Compiler
[1588]: https://github.com/enso-org/ide/pull/1588
[1577]: https://github.com/enso-org/ide/pull/1577
[1587]: https://github.com/enso-org/ide/pull/1587
[1602]: https://github.com/enso-org/ide/pull/1602
[1602]: https://github.com/enso-org/ide/pull/1664
[1602]: https://github.com/enso-org/ide/pull/1650
[1602]: https://github.com/enso-org/ide/pull/1626
# Enso 2.0.0-alpha.5 (2021-05-14)
<br/>![New Features](/docs/assets/tags/new_features.svg)
#### Visual Environment
- [Create New Project action in Searcher][1566]. When you bring the searcher
with tab having no node selected, a new action will be available next to the
examples and code suggestions: `Create New Project`. When you choose it by
clicking with mouse or selecting and pressing enter, a new unnamed project
will be created and opened in the application. Then you can give a name to
this project.
- [Signed builds.][1366] Our builds are signed and will avoid warnings from the
operating system about being untrusted.
#### EnsoGL (rendering engine)
- [Components for picking numbers and ranges.][1524]. We now have some internal
re-usable UI components for selecting numbers or a range. Stay tuned for them
appearing in the IDE.
<br/>![Bug Fixes](/docs/assets/tags/bug_fixes.svg)
#### Visual Environment
- [Delete key will delete selected nodes][1538]. Only the non-intuitive
backspace key was assigned to this action before.
- [It is possible to move around after deleting a node with a selected
visualization][1556]. Deleting a node while its attached visualization was
selected made it impossible to pan or zoom around the stage afterwards. This
error is fixed now.
- [Fixed an internal error that would make the IDE fail on some browser.][1561].
Instead of crashing on browser that don't support the feature we use, we are
now just start a little bit slower.
#### Enso Compiler
- [Updated Enso engine to version 0.2.11][1541].
If you're interested in the enhancements and fixes made to the Enso compiler,
you can find their release notes
[here](https://github.com/enso-org/enso/blob/develop/RELEASES.md).
[1366]: https://github.com/enso-org/ide/pull/1366
[1541]: https://github.com/enso-org/ide/pull/1541
[1538]: https://github.com/enso-org/ide/pull/1538
[1524]: https://github.com/enso-org/ide/pull/1524
[1556]: https://github.com/enso-org/ide/pull/1556
[1561]: https://github.com/enso-org/ide/pull/1561
[1566]: https://github.com/enso-org/ide/pull/1566
<br/>
# Enso 2.0.0-alpha.4 (2021-05-04)
<br/>![New Features](/docs/assets/tags/new_features.svg)
#### Visual Environment
- [Window management buttons.][1511]. The IDE now has components for
"fullscreen" and "close" buttons. They will when running IDE in a cloud
environment where no native window buttons are available.
- [Customizable backend options][1531]. When invoking Enso IDE through command
line interface, it is possible to add the `--` argument separator. All
arguments following the separator will be passed to the backend.
- [Added `--verbose` parameter][1531]. If `--verbose` is given as command line
argument, the IDE and the backend will produce more detailed logs.
<br/>![Bug Fixes](/docs/assets/tags/bug_fixes.svg)
#### Visual Environment
- [Some command line arguments were not applied correctly in the IDE][1536].
Some arguments were not passed correctly to the IDE leading to erroneous
behavior or appearance of the electron app. This is now fixed.
#### Enso Compiler
If you're interested in the enhancements and fixes made to the Enso compiler,
you can find their release notes
[here](https://github.com/enso-org/enso/blob/develop/RELEASES.md).
[1511]: https://github.com/enso-org/ide/pull/1511
[1536]: https://github.com/enso-org/ide/pull/1536
[1531]: https://github.com/enso-org/ide/pull/1531
<br/>
# Enso 2.0.0-alpha.3 (2020-04-13)
<br/>![New Learning Resources](/docs/assets/tags/new_learning_resources.svg)
<br/>![New Features](/docs/assets/tags/new_features.svg)
#### Visual Environment
- [The status bar reports connectivity issues][1316]. The IDE maintains a
connection to the Enso Language Server. If this connection is lost, any
unsaved and further work will be lost. In this build we have added a
notification in the status bar to signal that the connection has been lost and
that the IDE must be restarted. In future, the IDE will try to automatically
reconnect.
- [Visualizations can now be maximised to fill the screen][1355] by selecting
the node and pressing space twice. To quit this view, press space again.
- [Visualizations are previewed when you hover over an output port.][1363] There
is now a quick preview for visualizations and error descriptions. Hovering
over a node output will first show a tooltip with the type information and
then, after some time, will show the visualization of the node. This preview
visualization will be located above other nodes, whereas the normal view, will
be shown below nodes. Errors will show the preview visualization immediately.
Nodes without type information will also show the visualization immediately.
You can enter a quick preview mode by pressing ctrl (or command on macOS),
which will show the preview visualization immediately when hovering above a
node's output port.
- [Database Visualizations][1335]. Visualizations for the Database library have
been added. The Table visualization now automatically executes the underlying
query to display its results as a table. In addition, the SQL Query
visualization allows the user to see the query that is going to be run against
the database.
- [Histogram and Scatter Plot now support Dataframes.][1377] The `Table` and
`Column` datatypes are properly visualized. Scatter Plot can display points of
different colors, shapes and sizes, all as defined by the data within the
`Table`.
- [Many small visual improvements.][1419] See the source issue for more details.
- The dark theme is officially supported now. You can start the IDE with the
`--theme=dark` option to enable it.
- You can hide the node labels with the `--no-node-labels` option. This is
useful when creating demo videos.
- [Added a Heatmap visualization.][1438] Just as for the Scatter Plot, it
supports visualizing `Table`, but also `Vector`.
- [Add a background to the status bar][1447].
- [Display breadcrumbs behind nodes and other objects][1471].
- [Image visualization.][1367]. Visualizations for the Enso Image library. Now
you can display the `Image` type and a string with an image encoded in base64.
The histogram visualization has been adjusted, allowing you to display the
values of the precomputed bins, which is useful when the dataset is relatively
big, and it's cheaper to send the precomputed bins rather than the entire
dataset.
- [Output type labels.][1427] The labels, that show the output type of a node on
hover, appear now in a fixed position right below the node, instead of a
pop-up, as they did before.
<br/>![Bug Fixes](/docs/assets/tags/bug_fixes.svg)
#### Visual Environment
- [Not adding spurious imports][1209]. Fixed cases where the IDE was adding
unnecessary library imports when selecting hints from the node searcher. This
makes the generated textual code much easier to read, and reduces the
likelihood of accidental name collisions.
- [Hovering over an output port shows a pop-up with the result type of a
node][1312]. This allows easy discovery of the result type of a node, which
can help with both debugging and development.
- [Visualizations can define the context for preprocessor evaluation][1291].
Users can now decide which module's context should be used for visualization
preprocessor. This allows providing visualizations with standard library
functionalities or defining utilities that are shared between multiple
visualizations.
- [Fixed an issue with multiple instances of the IDE running.][1314] This fixes
an issue where multiple instances of the IDE (or even other applications)
could lead to the IDE not working.
- [Allow JS to log arbitrary objects.][1313] Previously using `console.log` in a
visualisation or during development would crash the IDE. Now it correctly logs
the string representation of the object. This is great for debugging custom
visualizations.
- [Fix the mouse cursor offset on systems with fractional display
scaling][1064]. The cursor now works with any display scaling, instead of
there being an offset between the visible cursor and the cursor selection.
- [Disable area selection][1318]. The area selection was visible despite being
non-functional. To avoid confusion, area selection has been disabled until it
is [correctly implemented][479].
- [Fix an error after adding a node][1332]. Sometimes, after picking a
suggestion, the inserted node was spuriously annotated with "The name could
not be found" error.
- [Handle syntax errors in custom-defined visualizations][1341]. The IDE is now
able to run properly, even if some of the custom visualizations inside a
project contain syntax errors.
- [Fix issues with pasting multi-line text into single-line text fields][1348].
The line in the copied text will be inserted and all additional lines will be
ignored.
- [Users can opt out of anonymous data gathering.][1328] This can be done with
the `--no-data-gathering` command-line flag when starting the IDE.
- [Provide a theming API for JavaScript visualizations][1358]. It is now
possible to use the Enso theming engine while developing custom visualizations
in JavaScript. You can query it for all IDE colors, including the colors used
to represent types.
- [You can now start the IDE service without a window again.][1353] The command
line argument `--no-window` now starts all the required backend services
again, and prints the port on the command line. This allows you to open the
IDE in a web browser of your choice.
- [JS visualizations have gestures consistent with the IDE][1291]. Panning and
zooming now works just as expected using both a trackpad and mouse.
- [Running `watch` command works on first try.][1395]. Running the build command
`run watch` would fail if it was run as the first command on a clean
repository. This now works.
- [The `inputType` field of visualizations is actually taken into
consideration][1384]. The visualization chooser shows only the entries that
work properly for the node's output type.
- [Fix applying the output of the selected node to the expression of a new
node][1385]. For example, having selected a node with `Table` output and
adding a new node with expression `at "x" == "y"`, the selected node was
applied to the right side of `==`: `at "x" == operator1."y"` instead of
`operator1.at "x" == "y"`.
- [`Enso_Project.data` is visible in the searcher][1393].
- [The Geo Map visualization recognizes columns regardless of the case of their
name][1392]. This allows visualizing tables with columns like `LONGITUDE` or
`Longitude`, where previously only `longitude` was recognized.
- [It is possible now to switch themes][1390]. Additionally, the theme manager
was integrated with the FRP event engine, which has been a long-standing issue
in the IDE. Themes management was exposed to JavaScript with the
`window.theme` variable. It is even possible to change and develop themes live
by editing theme variables directly in the Chrome Inspector. Use the following
command to give this a go:
`theme.snapshot("t1"); theme.get("t1").interactiveMode()`.
- [The active visualization is highlighted.][1412] Now it is clearly visible
when the mouse events are passed to the visualization.
- [Fixed an issue where projects containing certain language constructs failed
to load.][1413]
- [Fixed a case where IDE could lose connection to the backend after some
time.][1428]
- [Improved the performance of the graph editor, particularly when opening a
project for the first time.][1445]
#### EnsoGL (rendering engine)
- [Unified shadow generation][1411]. Added a toolset to create shadows for
arbitrary UI components.
#### Enso Compiler
If you're interested in the enhancements and fixes made to the Enso compiler,
you can find their release notes
[here](https://github.com/enso-org/enso/blob/develop/RELEASES.md#enso-0210-2021-04-07).
[1064]: https://github.com/enso-org/ide/pull/1064
[1209]: https://github.com/enso-org/ide/pull/1209
[1291]: https://github.com/enso-org/ide/pull/1291
[1311]: https://github.com/enso-org/ide/pull/1311
[1313]: https://github.com/enso-org/ide/pull/1313
[1314]: https://github.com/enso-org/ide/pull/1314
[1316]: https://github.com/enso-org/ide/pull/1316
[1318]: https://github.com/enso-org/ide/pull/1318
[1328]: https://github.com/enso-org/ide/pull/1328
[1355]: https://github.com/enso-org/ide/pull/1355
[1332]: https://github.com/enso-org/ide/pull/1332
[1341]: https://github.com/enso-org/ide/pull/1341
[1341]: https://github.com/enso-org/ide/pull/1341
[1348]: https://github.com/enso-org/ide/pull/1348
[1353]: https://github.com/enso-org/ide/pull/1353
[1395]: https://github.com/enso-org/ide/pull/1395
[1363]: https://github.com/enso-org/ide/pull/1363
[1384]: https://github.com/enso-org/ide/pull/1384
[1385]: https://github.com/enso-org/ide/pull/1385
[1390]: https://github.com/enso-org/ide/pull/1390
[1392]: https://github.com/enso-org/ide/pull/1392
[1393]: https://github.com/enso-org/ide/pull/1393
[479]: https://github.com/enso-org/ide/issues/479
[1335]: https://github.com/enso-org/ide/pull/1335
[1358]: https://github.com/enso-org/ide/pull/1358
[1377]: https://github.com/enso-org/ide/pull/1377
[1411]: https://github.com/enso-org/ide/pull/1411
[1412]: https://github.com/enso-org/ide/pull/1412
[1419]: https://github.com/enso-org/ide/pull/1419
[1413]: https://github.com/enso-org/ide/pull/1413
[1428]: https://github.com/enso-org/ide/pull/1428
[1438]: https://github.com/enso-org/ide/pull/1438
[1367]: https://github.com/enso-org/ide/pull/1367
[1445]: https://github.com/enso-org/ide/pull/1445
[1447]: https://github.com/enso-org/ide/pull/1447
[1471]: https://github.com/enso-org/ide/pull/1471
[1511]: https://github.com/enso-org/ide/pull/1511
<br/>
# Enso 2.0.0-alpha.2 (2020-03-04)
This is a release focused on bug-fixing, stability, and performance. It improves
the performance of workflows and visualizations, and improves the look and feel
of the graphical interface. In addition, the graphical interface now informs the
users about errors and where they originate.
<br/>![New Learning Resources](/docs/assets/tags/new_learning_resources.svg)
- [Learn how to define custom data visualizations in
Enso][podcast-custom-visualizations].
- [Learn how to use Java libraries in Enso, to build a
webserver][podcast-java-interop].
- [Learn how to use Javascript libraries in Enso, to build custom server-side
website rendering][podcast-http-server].
- [Discover why Enso Compiler is so fast and how it was built to support a
dual-representation language][podcast-compiler-internals].
- [Learn more about the vision behind Enso and about its planned
future][podcast-future-of-enso].
<br/>![New Features](/docs/assets/tags/new_features.svg)
#### Visual Environment
- [Errors in workflows are now displayed in the graphical interface][1215].
Previously, these errors were silently skipped, which was non-intuitive and
hard to understand. Now, the IDE displays both dataflow errors and panics in a
nice and descriptive fashion.
- [Added geographic map support for Tables (data frames).][1187] Tables that
have `latitude`, `longitude`, and optionally `label` columns can now be shown
as points on a map.
- [Added a shortcut for live reloading of visualization files.][1190] This
drastically improves how quickly new visualizations can be tested during their
development. This is _currently_ limited in that, after reloading
visualization definitions, the currently visible visualizations must be
switched to another and switched back to refresh their content. See the [video
podcast about building custom visualizations][podcast-custom-visualizations]
to learn more.
- [Added a visual indicator of the ongoing standard library compilation][1264].
Currently, each time IDE is started, the backend needs to compile the standard
library before it can provide IDE with type information and values. Because of
that, not all functionalities are ready to work directly after starting the
IDE. Now, there is a visible indication of the ongoing background process.
- [Added the ability to reposition visualisations.][1096] There is now an icon
in the visualization action bar that allows dragging the visualization away
from a node. Once the visualization has been moved, another icon appears that
can pin the visualization back to the node.
- [There is now an API to show Version Control System (like Git) status for
nodes][1160].
<br/>![Bug Fixes](/docs/assets/tags/bug_fixes.svg)
#### Visual Environment
- [You can now use the table visualization to display data frames][1181]. Please
note, that large tables will get truncated to 2000 entries. This limitation
will be lifted in future releases.
- [Performance improvements during visual workflow][1067]. Nodes added with the
searcher will have their values automatically assigned to newly generated
variables, which allows the Enso Engine to cache intermediate values and hence
improve visualization performance.
- [Minor documentation rendering fixes][1098]. Fixed cases where text would be
misinterpreted as a tag, added support for new tag types, added support for
more common characters, properly renders overflowing text.
- [Improved handling of projects created with other IDE versions][1214]. The IDE
is now better at dealing with incompatible metadata in files, which stores
node visual position information, the history of chosen searcher suggestions,
etc. This will allow IDE to correctly open projects that were created using a
different IDE version and prevent unnecessary loss of metadata.
- Pressing and holding up and down arrow keys make the list view selection move
continuously.
- The shortcuts to close the application and to toggle the developer tools at
runtime now work on all supported platforms.
- [The loading progress indicator remains visible while IDE initializes][1237].
Previously the loading progress indicator completed too quickly and stopped
spinning before the IDE was ready. Now it stays active, giving a visual
indication that the initialization is still in progress.
- [Fixed visual glitch where a node's text was displayed as white on a white
background][1264]. Most notably this occurred with the output node of a
function generated using the node collapse refactoring.
- Many visual glitches were fixed, including small "pixel-like" artifacts
appearing on the screen.
- [Several parser improvements][1274]. The parser used in the IDE has been
updated to the latest version. This resolves several issues with language
constructs like `import`, lambdas, and parentheses, whereupon typing certain
text the edit could be automatically reverted.
- [The auto-import functionality was improved][1279]. Libraries' `Main` modules
are omitted in expressions inserted by the searcher. For example, the `point`
method of `Geo` library will be displayed as `Geo.point` and will insert
import `Geo` instead of `Geo.Main`.
- Cursors in text editors behave correctly now (they are not affected by scene
pan and zoom). This was possible because of the new multi-camera management
system implemented in EnsoGL.
- [Fixed method names highlighted in pink.][1408] There was a bug introduced
after one of the latest Engine updates, that sent `Unresolved_symbol` types,
which made all methods pink. This is fixed now.
#### EnsoGL (rendering engine)
- A new multi-camera management system, allowing the same shape systems to be
rendered on different layers from different cameras. The implementation
automatically caches the same shape system definitions per scene layer in
order to minimize the amount of WebGL draw calls and hence improve
performance.
- A new depth-ordering mechanism for symbols and shapes. It is now possible to
define depth order dependencies between symbols, shapes, and shape systems.
- Various performance improvements, especially for the text rendering engine.
- Display objects handle visibility correctly now. Display objects are not
visible by default and need to be attached to a visible parent to be shown on
the screen.
#### Enso Compiler
If you're interested in the enhancements and fixes made to the Enso compiler,
you can find their release notes
[here](https://github.com/enso-org/enso/blob/develop/RELEASES.md#enso-026-2021-03-02).
[1067]: https://github.com/enso-org/ide/pull/1067
[1096]: https://github.com/enso-org/ide/pull/1096
[1098]: https://github.com/enso-org/ide/pull/1098
[1181]: https://github.com/enso-org/ide/pull/1181
[1215]: https://github.com/enso-org/ide/pull/1215
[1160]: https://github.com/enso-org/ide/pull/1160
[1190]: https://github.com/enso-org/ide/pull/1190
[1187]: https://github.com/enso-org/ide/pull/1187
[1068]: https://github.com/enso-org/ide/pull/1068
[1214]: https://github.com/enso-org/ide/pull/1214
[1237]: https://github.com/enso-org/ide/pull/1237
[1264]: https://github.com/enso-org/ide/pull/1264
[1274]: https://github.com/enso-org/ide/pull/1274
[1279]: https://github.com/enso-org/ide/pull/1279
[podcast-java-interop]:
https://www.youtube.com/watch?v=bcpOEX1x06I&t=468s&ab_channel=Enso
[podcast-compiler-internals]:
https://www.youtube.com/watch?v=BibjcUjdkO4&ab_channel=Enso
[podcast-custom-visualizations]:
https://www.youtube.com/watch?v=wFkh5LgAZTs&t=5439s&ab_channel=Enso
[podcast-http-server]:
https://www.youtube.com/watch?v=BYUAL4ksEgY&ab_channel=Enso
[podcast-future-of-enso]:
https://www.youtube.com/watch?v=rF8DuJPOfTs&t=1863s&ab_channel=Enso
[1312]: https://github.com/enso-org/ide/pull/1312
[1408]: https://github.com/enso-org/ide/pull/1408
<br/>
# Enso 2.0.0-alpha.1 (2020-01-26)
This is the first release of Enso, a general-purpose programming language and
environment for interactive data processing. It is a tool that spans the entire
stack, going from high-level visualization and communication to the nitty-gritty
of backend services, all in a single language.
<br/>![Release Notes](/docs/assets/tags/release_notes.svg)
#### Anonymous Data Collection
Please note that this release collects anonymous usage data which will be used
to improve Enso and prepare it for a stable release. We will switch to opt-in
data collection in stable version releases. The usage data will not contain your
code (expressions above nodes), however, reported errors may contain brief
snippets of out of context code that specifically leads to the error, like "the
method 'foo' does not exist on Number". The following data will be collected:
- Session length.
- Graph editing events (node create, dele, position change, connect, disconnect,
collapse, edit start, edit end). This will not include any information about
node expressions used.
- Navigation events (camera movement, scope change).
- Visualization events (visualization open, close, switch). This will not
include any information about the displayed data nor the rendered
visualization itself.
- Project management events (project open, close, rename).
- Errors (IDE crashes, WASM panics, Project Manager errors, Language Server
errors, Compiler errors).
- Performance statistics (minimum, maximum, average GUI refresh rate).

File diff suppressed because it is too large Load Diff

656
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,10 @@ members = [
"build/macros/proc-macro",
"build/ci-gen",
"build/cli",
"build/install",
"build/install/config",
"build/install/installer",
"build/install/uninstaller",
"lib/rust/*",
"lib/rust/parser/doc-parser",
"lib/rust/parser/src/syntax/tree/visitor",
@ -89,23 +93,38 @@ blocks_in_conditions = "allow" # Until the issue is fixed: https://github.com/ru
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# We are tryingto maintain minimum set of dependencies. Before adding a new dependency, consult it
# with the core development team. Thank you!
chrono = { version = "0.4.31", features = ["serde"] }
clap = { version = "4.5.4", features = ["derive", "env", "wrap_help", "string"] }
derive-where = "1.2.7"
directories = { version = "5.0.1" }
dirs = { version = "5.0.1" }
flate2 = { version = "1.0.28" }
indicatif = { version = "0.17.7", features = ["tokio"] }
multimap = { version = "0.9.1" }
native-windows-gui = { version = "1.0.13" }
nix = { version = "0.27.1" }
octocrab = { git = "https://github.com/enso-org/octocrab", default-features = false, features = [
"rustls",
] }
path-absolutize = "3.1.1"
platforms = { version = "3.2.0", features = ["serde"] }
portpicker = { version = "0.1.1" }
regex = { version = "1.6.0" }
serde = { version = "1.0.130", features = ["derive", "rc"] }
serde_yaml = { version = "0.9.16" }
sha2 = { version = "0.10.8" }
sysinfo = { version = "0.30.7" }
tokio = { version = "1.23.0", features = ["full", "tracing"] }
tokio-stream = { version = "0.1.12", features = ["fs"] }
tokio-util = { version = "0.7.4", features = ["full"] }
tar = { version = "0.4.40" }
tokio = { version = "1.37.0", features = ["full", "tracing"] }
tokio-stream = { version = "0.1.15", features = ["fs"] }
tokio-util = { version = "0.7.10", features = ["full"] }
tracing = { version = "0.1.40" }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
walkdir = { version = "2.5.0" }
wasm-bindgen = { version = "0.2.92", features = [] }
wasm-bindgen-test = { version = "0.3.34" }
windows = { version = "0.52.0", features = ["Win32", "Win32_UI", "Win32_UI_Shell", "Win32_System", "Win32_System_LibraryLoader", "Win32_Foundation", "Win32_System_Com"] }
winreg = { version = "0.52.0" }
anyhow = { version = "1.0.66" }
failure = { version = "0.1.8" }
derive_more = { version = "0.99" }

View File

@ -1,9 +1,9 @@
"enso-org/enso"
bazel_dep(name = "aspect_rules_js", version = "1.41.2")
bazel_dep(name = "aspect_rules_js", version = "2.0.0-rc6")
####### Node.js version #########
bazel_dep(name = "rules_nodejs", version = "6.1.0")
bazel_dep(name = "rules_nodejs", version = "6.2.0")
node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node")
node.toolchain(node_version = "20.11.1")
#################################

View File

@ -1,6 +1,6 @@
{
"lockFileVersion": 6,
"moduleFileHash": "a055a04b9c110bf06e81dd03cace78357d354ece7f4ca2bcf2f384a31fb5903a",
"moduleFileHash": "84c1c4450d4799f54ff999c80b2d8b836c19ce9c5400bad59bb8dd138066d28b",
"flags": {
"cmdRegistries": [
"https://bcr.bazel.build/"
@ -159,7 +159,7 @@
"usingModule": "<root>",
"location": {
"file": "@@//:MODULE.bazel",
"line": 69,
"line": 70,
"column": 22
},
"imports": {
@ -199,7 +199,7 @@
"devDependency": false,
"location": {
"file": "@@//:MODULE.bazel",
"line": 72,
"line": 73,
"column": 14
}
}

View File

@ -21,7 +21,7 @@
<img src="https://img.shields.io/static/v1?label=Compiler%20License&message=Apache%20v2&color=2ec352&labelColor=2c3239"
alt="License">
</a>
<a href="https://github.com/enso-org/enso/blob/develop/app/gui/LICENSE">
<a href="https://github.com/enso-org/enso/blob/develop/app/gui2/LICENSE">
<img src="https://img.shields.io/static/v1?label=GUI%20License&message=AGPL%20v3&color=2ec352&labelColor=2c3239"
alt="License">
</a>
@ -207,11 +207,11 @@ Enso consists of several sub projects:
command line tools.
- **Enso IDE:** The
[Enso IDE](https://github.com/enso-org/enso/tree/develop/app/gui) is a desktop
application that allows working with the visual form of Enso. It consists of
an Electron application, a high performance WebGL UI framework, and the
searcher which provides contextual search, hints, and documentation for all of
Enso's functionality.
[Enso IDE](https://github.com/enso-org/enso/tree/develop/app/gui2) is a
desktop application that allows working with the visual form of Enso. It
consists of an Electron application, a high performance WebGL UI framework,
and the searcher which provides contextual search, hints, and documentation
for all of Enso's functionality.
<br/>
@ -222,7 +222,7 @@ The Enso Engine is licensed under the
[LICENSE](https://github.com/enso-org/enso/blob/develop/LICENSE) file. The Enso
IDE is licensed under the [AGPL 3.0](https://opensource.org/licenses/AGPL-3.0),
as specified in the
[LICENSE](https://github.com/enso-org/enso/blob/develop/app/gui/LICENSE) file.
[LICENSE](https://github.com/enso-org/enso/blob/develop/app/gui2/LICENSE) file.
This license set was chosen to provide you with complete freedom to use Enso,
create libraries, and release them under any license of your choice, while also

View File

@ -1,4 +1,4 @@
ENSO_CLOUD_REDIRECT=http://localhost:8080
ENSO_CLOUD_ENSO_HOST=https://ensoanalytics.com
ENSO_CLOUD_ENVIRONMENT=production
ENSO_CLOUD_API_URL=https://aaaaaaaaaa.execute-api.mars.amazonaws.com
ENSO_CLOUD_CHAT_URL=wss://chat.example.com

View File

@ -17,9 +17,6 @@ internal folder structure of the module.
- [`dashboard/`](./lib/dashboard/README.md): The dashboard, used to manage
projects. It launches the GUI (located in `content/` for GUI1, or `/app/gui2/`
for GUI2) when a project is opened.
- `esbuild-plugin-copy-directories/`: An ESBuild plugin for continuously copying
directories from the a given location to a given subdirectory of the build
output directory.
- `icons/`: Generates the logo for the app.
- `ts-plugin-namespace-auto-import/`: (WIP) A TypeScript plugin to change
auto-import to use `import * as moduleName` rather than `import {}`.

View File

@ -22,6 +22,8 @@ module.exports = {
'',
'^enso-',
'',
'^#[/]assets',
'',
'^#[/]App',
'^#[/]appUtils',
'^#[/]text',

View File

@ -36,16 +36,11 @@
<script type="module" src="./src/entrypoint.ts" defer></script>
</head>
<body>
<div id="app"></div>
<div id="enso-dashboard" class="enso-dashboard"></div>
<div id="enso-chat" class="enso-chat"></div>
<div id="enso-portal-root" class="enso-portal-root"></div>
<noscript>
This page requires JavaScript to run. Please enable it in your browser.
</noscript>
<!-- Google tag (gtag.js) -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-CLTBJ37MDM"
></script>
</body>
</html>

View File

@ -3,6 +3,13 @@
The dashboard is the entrypoint into the application. It includes project
management, project sharing, and user accounts and authentication.
## Further documentation
Further documentation is provided in the `docs/` folder:
- [Browser-specific behavior](./docs/browser_specific_behavior.md) details
behavior that is inconsistent between browsers and needs to be worked around.
## Folder structure
- `mock/`: Overrides for specific files in `src/` when running Playwright tests.

View File

@ -0,0 +1,65 @@
# Browser-specific behavior
This document details behavior that is inconsistent between browsers and needs
to be worked around.
## List of inconsistent behaviors
### Drag event missing coordinates
Firefox sets `MouseEvent.pageX` and `MouseEvent.pageY` to `0` for `drag`
events.
#### Fix
Pass the `drag` event handlers to `dragover` event as well, and wrap all `drag`
event handlers in:
````ts
if (event.pageX !== 0 || event.pageY !== 0) {
// original body here
}
```
#### Affected files
- [`DragModal.tsx`](../src/modals/DragModal.tsx)
### Drag event propagation in text inputs
Text selection in text inputs DO NOT WORK on Firefox, when the text input is a
child of an element with `draggable="true"`.
See [Firefox bug 800050].
To solve this problem, use `useDraggable` from
[`dragAndDropHooks.ts`] on ALL elements that MAY contain a text input.
[Firefox bug 800050]: https://bugzilla.mozilla.org/show_bug.cgi?id=800050
#### Fix
Merge `useDraggable` from [`dragAndDropHooks.ts`] on ALL elements that MAY
contain a text input.
It is recommended to use `aria.mergeProps` to combine these props with existing
props.
```tsx
import * as dragAndDropHooks from "#/hooks/dragAndDropHooks.ts";
const draggableProps = dragAndDropHooks.useDraggable();
return <div {...draggableProps}></div>;
````
[`draggableHooks.ts`]: ../src/hooks/dragAndDropHooks.ts
#### Affected browsers
- Firefox (all versions)
#### Affected files
- [`EditableSpan.tsx`](../src/components/EditableSpan.tsx) - the text inputs
that are affected
- [`AssetRow.tsx`](../src/components/dashboard/AssetRow.tsx) - fixes text
selection in `EditableSpan.tsx`

View File

@ -0,0 +1,53 @@
# End-to-end tests
## Running tests
Execute all commands from the parent directory.
```sh
# Run tests normally
npm run test:e2e
# Open UI to run tests
npm run test:e2e:debug
# Run tests in a specific file only
npm run test:e2e -- e2e/file-name-here.spec.ts
npm run test:e2e:debug -- e2e/file-name-here.spec.ts
# Compile the entire app before running the tests.
# DOES NOT hot reload the tests.
# Prefer not using this when you are trying to fix a test;
# prefer using this when you just want to know which tests are failing (if any).
PROD=1 npm run test:e2e
PROD=1 npm run test:e2e:debug
PROD=1 npm run test:e2e -- e2e/file-name-here.spec.ts
PROD=1 npm run test:e2e:debug -- e2e/file-name-here.spec.ts
```
## Getting started
```ts
test.test("test name here", ({ page }) =>
actions.mockAllAndLogin({ page }).then(
// ONLY chain methods from `pageActions`.
// Using methods not in `pageActions` is UNDEFINED BEHAVIOR.
// If it is absolutely necessary though, please remember to `await` the method chain.
// Note that the `async`/`await` pair is REQUIRED, as `Actions` subclasses are `PromiseLike`s,
// not `Promise`s, which causes Playwright to output a type error.
async ({ pageActions }) => await pageActions.goTo.drive(),
),
);
```
### Perform arbitrary actions (e.g. actions on the API)
```ts
test.test("test name here", ({ page }) =>
actions.mockAllAndLogin({ page }).then(
async ({ pageActions, api }) =>
await pageActions.do(() => {
api.foo();
api.bar();
test.expect(api.baz()?.quux).toEqual("bar");
}),
),
);
```

View File

@ -1,8 +1,13 @@
/* eslint-disable @typescript-eslint/no-redeclare */
/** @file Various actions, locators, and constants used in end-to-end tests. */
import * as test from '@playwright/test'
import DrivePageActions from './actions/DrivePageActions'
import LoginPageActions from './actions/LoginPageActions'
import * as apiModule from './api'
/* eslint-disable @typescript-eslint/no-namespace */
// =================
// === Constants ===
// =================
@ -35,21 +40,6 @@ export function locateConfirmPasswordInput(page: test.Locator | test.Page) {
return page.getByPlaceholder('Confirm your password')
}
/** Find a "current password" input (if any) on the current page. */
export function locateCurrentPasswordInput(page: test.Locator | test.Page) {
return page.getByPlaceholder('Enter your current password')
}
/** Find a "new password" input (if any) on the current page. */
export function locateNewPasswordInput(page: test.Locator | test.Page) {
return page.getByPlaceholder('Enter your new password')
}
/** Find a "confirm new password" input (if any) on the current page. */
export function locateConfirmNewPasswordInput(page: test.Locator | test.Page) {
return page.getByPlaceholder('Confirm your new password')
}
/** Find a "username" input (if any) on the current page. */
export function locateUsernameInput(page: test.Locator | test.Page) {
return page.getByPlaceholder('Enter your username')
@ -82,9 +72,7 @@ export function locateSecretValueInput(page: test.Page) {
/** Find a search bar input (if any) on the current page. */
export function locateSearchBarInput(page: test.Page) {
return locateSearchBar(page).getByPlaceholder(
'Type to search for projects, Data Links, users, and more.'
)
return locateSearchBar(page).getByPlaceholder(/(?:)/)
}
/** Find the name column of the given assets table row. */
@ -94,13 +82,6 @@ export function locateAssetRowName(locator: test.Locator) {
// === Button locators ===
/** Find a toast close button (if any) on the current locator. */
export function locateToastCloseButton(page: test.Locator | test.Page) {
// There is no other simple way to uniquely identify this element.
// eslint-disable-next-line no-restricted-properties
return page.locator('.Toastify__close-button')
}
/** Find a "login" button (if any) on the current locator. */
export function locateLoginButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Login', exact: true }).getByText('Login')
@ -111,21 +92,6 @@ export function locateRegisterButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Register' }).getByText('Register')
}
/** Find a "change" button (if any) on the current locator. */
export function locateChangeButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Change' }).getByText('Change')
}
/** Find a user menu button (if any) on the current locator. */
export function locateUserMenuButton(page: test.Locator | test.Page) {
return page.getByAltText('Open user menu').locator('visible=true')
}
/** Find a "sign out" button (if any) on the current locator. */
export function locateLogoutButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Logout' }).getByText('Logout')
}
/** Find a "set username" button (if any) on the current page. */
export function locateSetUsernameButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Set Username' }).getByText('Set Username')
@ -148,43 +114,41 @@ export function locateCreateButton(page: test.Locator | test.Page) {
/** Find a button to open the editor (if any) on the current page. */
export function locatePlayOrOpenProjectButton(page: test.Locator | test.Page) {
return page.getByAltText('Open in editor')
return page.getByLabel('Open in editor')
}
/** Find a button to close the project (if any) on the current page. */
export function locateStopProjectButton(page: test.Locator | test.Page) {
return page.getByAltText('Stop execution')
return page.getByLabel('Stop execution')
}
/** Close a modal. */
export function closeModal(page: test.Page) {
return test.test.step('Close modal', async () => {
await page.getByLabel('Close').click()
})
}
/** Find all labels in the labels panel (if any) on the current page. */
export function locateLabelsPanelLabels(page: test.Page) {
export function locateLabelsPanelLabels(page: test.Page, name?: string) {
return (
locateLabelsPanel(page)
.getByRole('button')
.filter(name != null ? { has: page.getByText(name) } : {})
// The delete button is also a `button`.
// eslint-disable-next-line no-restricted-properties
.and(page.locator(':nth-child(1)'))
)
}
/** Find a "home" button (if any) on the current page. */
export function locateHomeButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Home' }).getByText('Home')
}
/** Find a "trash" button (if any) on the current page. */
export function locateTrashButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Trash' }).getByText('Trash')
}
/** Find a tick button (if any) on the current page. */
export function locateEditingTick(page: test.Locator | test.Page) {
return page.getByAltText('Confirm Edit')
return page.getByLabel('Confirm Edit')
}
/** Find a cross button (if any) on the current page. */
export function locateEditingCross(page: test.Locator | test.Page) {
return page.getByAltText('Cancel Edit')
return page.getByLabel('Cancel Edit')
}
/** Find labels in the "Labels" column of the assets table (if any) on the current page. */
@ -194,162 +158,55 @@ export function locateAssetLabels(page: test.Locator | test.Page) {
/** Find a toggle for the "Name" column (if any) on the current page. */
export function locateNameColumnToggle(page: test.Locator | test.Page) {
return page.getByAltText(/^(?:Show|Hide) Name$/)
return page.getByAltText('Name')
}
/** Find a toggle for the "Modified" column (if any) on the current page. */
export function locateModifiedColumnToggle(page: test.Locator | test.Page) {
return page.getByAltText(/^(?:Show|Hide) Modified date column$/)
return page.getByAltText('Modified')
}
/** Find a toggle for the "Shared with" column (if any) on the current page. */
export function locateSharedWithColumnToggle(page: test.Locator | test.Page) {
return page.getByAltText(/^(?:Show|Hide) Shared with column$/)
return page.getByAltText('Shared With')
}
/** Find a toggle for the "Labels" column (if any) on the current page. */
export function locateLabelsColumnToggle(page: test.Locator | test.Page) {
return page.getByAltText(/^(?:Show|Hide) Labels column$/)
return page.getByAltText('Labels')
}
/** Find a toggle for the "Accessed by projects" column (if any) on the current page. */
export function locateAccessedByProjectsColumnToggle(page: test.Locator | test.Page) {
return page.getByAltText(/^(?:Show|Hide) Accessed by projects column$/)
return page.getByAltText('Accessed By Projects')
}
/** Find a toggle for the "Accessed data" column (if any) on the current page. */
export function locateAccessedDataColumnToggle(page: test.Locator | test.Page) {
return page.getByAltText(/^(?:Show|Hide) Accessed data column$/)
return page.getByAltText('Accessed Data')
}
/** Find a toggle for the "Docs" column (if any) on the current page. */
export function locateDocsColumnToggle(page: test.Locator | test.Page) {
return page.getByAltText(/^(?:Show|Hide) Docs column$/)
return page.getByAltText('Docs')
}
/** Find a button for the "Recent" category (if any) on the current page. */
export function locateRecentCategory(page: test.Locator | test.Page) {
return page.getByLabel('Go To Recent category')
return page.getByLabel('Recent').locator('visible=true')
}
/** Find a button for the "Home" category (if any) on the current page. */
export function locateHomeCategory(page: test.Locator | test.Page) {
return page.getByLabel('Go To Home category')
return page.getByLabel('Home').locator('visible=true')
}
/** Find a button for the "Trash" category (if any) on the current page. */
export function locateTrashCategory(page: test.Locator | test.Page) {
return page.getByLabel('Go To Trash category')
return page.getByLabel('Trash').locator('visible=true')
}
// === Context menu buttons ===
/** Find an "open" button (if any) on the current page. */
export function locateOpenButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Open' }).getByText('Open')
}
/** Find an "upload to cloud" button (if any) on the current page. */
export function locateUploadToCloudButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Upload To Cloud' }).getByText('Upload To Cloud')
}
/** Find a "rename" button (if any) on the current page. */
export function locateRenameButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Rename' }).getByText('Rename')
}
/** Find a "snapshot" button (if any) on the current page. */
export function locateSnapshotButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Snapshot' }).getByText('Snapshot')
}
/** Find a "move to trash" button (if any) on the current page. */
export function locateMoveToTrashButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Move To Trash' }).getByText('Move To Trash')
}
/** Find a "move all to trash" button (if any) on the current page. */
export function locateMoveAllToTrashButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Move All To Trash' }).getByText('Move All To Trash')
}
/** Find a "restore from trash" button (if any) on the current page. */
export function locateRestoreFromTrashButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Restore From Trash' }).getByText('Restore From Trash')
}
/** Find a "restore all from trash" button (if any) on the current page. */
export function locateRestoreAllFromTrashButton(page: test.Locator | test.Page) {
return page
.getByRole('button', { name: 'Restore All From Trash' })
.getByText('Restore All From Trash')
}
/** Find a "share" button (if any) on the current page. */
export function locateShareButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Share' }).getByText('Share')
}
/** Find a "label" button (if any) on the current page. */
export function locateLabelButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Label' }).getByText('Label')
}
/** Find a "duplicate" button (if any) on the current page. */
export function locateDuplicateButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Duplicate' }).getByText('Duplicate')
}
/** Find a "copy" button (if any) on the current page. */
export function locateCopyButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Copy' }).getByText('Copy')
}
/** Find a "cut" button (if any) on the current page. */
export function locateCutButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Cut' }).getByText('Cut')
}
/** Find a "paste" button (if any) on the current page. */
export function locatePasteButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Paste' }).getByText('Paste')
}
/** Find a "download" button (if any) on the current page. */
export function locateDownloadButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Download' }).getByText('Download')
}
/** Find a "download app" button (if any) on the current page. */
export function locateDownloadAppButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Download App' }).getByText('Download App')
}
/** Find an "upload files" button (if any) on the current page. */
export function locateUploadFilesButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Upload Files' }).getByText('Upload Files')
}
/** Find a "new project" button (if any) on the current page. */
export function locateNewProjectButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'New Project' }).getByText('New Project')
}
/** Find a "new folder" button (if any) on the current page. */
export function locateNewFolderButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'New Folder' }).getByText('New Folder')
}
/** Find a "new secret" button (if any) on the current page. */
export function locateNewSecretButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'New Secret' }).getByText('New Secret')
}
/** Find a "new data connector" button (if any) on the current page. */
export function locateNewDataConnectorButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'New Data Connector' }).getByText('New Data Connector')
}
// === Other buttons ===
/** Find a "new label" button (if any) on the current page. */
export function locateNewLabelButton(page: test.Locator | test.Page) {
@ -358,35 +215,27 @@ export function locateNewLabelButton(page: test.Locator | test.Page) {
/** Find an "upgrade" button (if any) on the current page. */
export function locateUpgradeButton(page: test.Locator | test.Page) {
return page.getByRole('link', { name: 'Upgrade', exact: true }).getByText('Upgrade')
return page.getByRole('link', { name: 'Upgrade', exact: true }).getByText('Upgrade').first()
}
/** Find a not enabled stub view (if any) on the current page. */
export function locateNotEnabledStub(page: test.Locator | test.Page) {
return page.getByTestId('not-enabled-stub')
}
/** Find a "new folder" icon (if any) on the current page. */
export function locateNewFolderIcon(page: test.Locator | test.Page) {
return page.getByAltText('New Folder')
return page.getByRole('button', { name: 'New Folder' })
}
/** Find a "new secret" icon (if any) on the current page. */
export function locateNewSecretIcon(page: test.Locator | test.Page) {
return page.getByAltText('New Secret')
}
/** Find a "upload files" icon (if any) on the current page. */
export function locateUploadFilesIcon(page: test.Locator | test.Page) {
return page.getByAltText('Upload Files')
return page.getByRole('button', { name: 'New Secret' })
}
/** Find a "download files" icon (if any) on the current page. */
export function locateDownloadFilesIcon(page: test.Locator | test.Page) {
return page.getByAltText('Download Files')
}
/** Find an icon to open or close the asset panel (if any) on the current page. */
export function locateAssetPanelIcon(page: test.Locator | test.Page) {
return page
.getByAltText('Open Asset Panel')
.or(page.getByAltText('Close Asset Panel'))
.locator('visible=true')
return page.getByRole('button', { name: 'Export' })
}
/** Find a list of tags in the search bar (if any) on the current page. */
@ -419,31 +268,14 @@ export function locateSortDescendingIcon(page: test.Locator | test.Page) {
return page.getByAltText('Sort Descending')
}
// === Page locators ===
/** Find a "home page" icon (if any) on the current page. */
export function locateHomePageIcon(page: test.Locator | test.Page) {
return page.getByAltText('Home tab')
}
/** Find a "drive page" icon (if any) on the current page. */
export function locateDrivePageIcon(page: test.Locator | test.Page) {
return page.getByAltText('Drive tab')
}
/** Find an "editor page" icon (if any) on the current page. */
export function locateEditorPageIcon(page: test.Locator | test.Page) {
return page.getByAltText('Project tab')
}
/** Find a "settings page" icon (if any) on the current page. */
export function locateSettingsPageIcon(page: test.Locator | test.Page) {
return page.getByAltText('Settings tab')
}
// === Heading locators ===
/** Find a "name" column heading (if any) on the current page. */
export function locateNameColumnHeading(page: test.Locator | test.Page) {
return page.getByLabel('Sort by name').or(page.getByLabel('Stop sorting by name'))
return page
.getByLabel('Sort by name')
.or(page.getByLabel('Stop sorting by name'))
.or(page.getByLabel('Sort by name descending'))
}
/** Find a "modified" column heading (if any) on the current page. */
@ -451,6 +283,7 @@ export function locateModifiedColumnHeading(page: test.Locator | test.Page) {
return page
.getByLabel('Sort by modification date')
.or(page.getByLabel('Stop sorting by modification date'))
.or(page.getByLabel('Sort by modification date descending'))
}
// === Container locators ===
@ -481,10 +314,8 @@ export function locateModalBackground(page: test.Locator | test.Page) {
/** Find an editor container (if any) on the current page. */
export function locateEditor(page: test.Page) {
// This is fine as this element is defined in `index.html`, rather than from React.
// Using `data-testid` may be more correct though.
// eslint-disable-next-line no-restricted-properties
return page.locator('#app')
// Test ID of a placeholder editor component used during testing.
return page.getByTestId('gui-editor-root')
}
/** Find an assets table (if any) on the current page. */
@ -505,13 +336,17 @@ export function locateAssetName(locator: test.Locator) {
/** Find assets table rows that represent directories that can be expanded (if any)
* on the current page. */
export function locateExpandableDirectories(page: test.Page) {
return locateAssetRows(page).filter({ has: page.getByAltText('Expand') })
// The icon is hidden when not hovered so `getByLabel` will not work.
// eslint-disable-next-line no-restricted-properties
return locateAssetRows(page).filter({ has: page.locator('[aria-label=Expand]') })
}
/** Find assets table rows that represent directories that can be collapsed (if any)
* on the current page. */
export function locateCollapsibleDirectories(page: test.Page) {
return locateAssetRows(page).filter({ has: page.getByAltText('Collapse') })
// The icon is hidden when not hovered so `getByLabel` will not work.
// eslint-disable-next-line no-restricted-properties
return locateAssetRows(page).filter({ has: page.locator('[aria-label=Collapse]') })
}
/** Find a "confirm delete" modal (if any) on the current page. */
@ -532,10 +367,15 @@ export function locateUpsertSecretModal(page: test.Page) {
return page.getByTestId('upsert-secret-modal')
}
/** Find a "new user group" modal (if any) on the current page. */
export function locateNewUserGroupModal(page: test.Page) {
// This has no identifying features.
return page.getByTestId('new-user-group-modal')
}
/** Find a user menu (if any) on the current page. */
export function locateUserMenu(page: test.Page) {
// This has no identifying features.
return page.getByTestId('user-menu')
return page.getByAltText('User Settings').locator('visible=true')
}
/** Find a "set username" panel (if any) on the current page. */
@ -565,7 +405,7 @@ export function locateLabelsList(page: test.Page) {
/** Find an asset panel (if any) on the current page. */
export function locateAssetPanel(page: test.Page) {
// This has no identifying features.
return page.getByTestId('asset-panel')
return page.getByTestId('asset-panel').locator('visible=true')
}
/** Find a search bar (if any) on the current page. */
@ -602,6 +442,177 @@ export function locateAssetPanelPermissions(page: test.Page) {
return locateAssetPanel(page).getByTestId('asset-panel-permissions').getByRole('button')
}
export namespace settings {
export namespace tab {
export namespace organization {
/** Find an "organization" tab button. */
export function locate(page: test.Page) {
return page.getByRole('button', { name: 'Organization' }).getByText('Organization')
}
}
export namespace members {
/** Find a "members" tab button. */
export function locate(page: test.Page) {
return page.getByRole('button', { name: 'Members', exact: true }).getByText('Members')
}
}
}
export namespace userAccount {
/** Navigate so that the "user account" settings section is visible. */
export async function go(page: test.Page) {
await test.test.step('Go to "user account" settings section', async () => {
await locateUserMenu(page).click()
await page.getByRole('button', { name: 'Settings' }).getByText('Settings').click()
})
}
/** Find a "user account" settings section. */
export function locate(page: test.Page) {
return page.getByRole('heading').and(page.getByText('User Account')).locator('..')
}
/** Find a "name" input in the "user account" settings section. */
export function locateNameInput(page: test.Page) {
return locate(page).getByLabel('Name')
}
}
export namespace changePassword {
/** Navigate so that the "change password" settings section is visible. */
export async function go(page: test.Page) {
await test.test.step('Go to "change password" settings section', async () => {
await locateUserMenu(page).click()
await page.getByRole('button', { name: 'Settings' }).getByText('Settings').click()
})
}
/** Find a "change password" settings section. */
export function locate(page: test.Page) {
return page.getByRole('heading').and(page.getByText('Change Password')).locator('..')
}
/** Find a "current password" input in the "user account" settings section. */
export function locateCurrentPasswordInput(page: test.Page) {
return locate(page).getByLabel('Current password')
}
/** Find a "new password" input in the "user account" settings section. */
export function locateNewPasswordInput(page: test.Page) {
return locate(page).getByLabel('New password', { exact: true })
}
/** Find a "confirm new password" input in the "user account" settings section. */
export function locateConfirmNewPasswordInput(page: test.Page) {
return locate(page).getByLabel('Confirm new password')
}
/** Find a "change" button. */
export function locateChangeButton(page: test.Page) {
return locate(page).getByRole('button', { name: 'Change' }).getByText('Change')
}
}
export namespace profilePicture {
/** Navigate so that the "profile picture" settings section is visible. */
export async function go(page: test.Page) {
await test.test.step('Go to "profile picture" settings section', async () => {
await locateUserMenu(page).click()
await page.getByRole('button', { name: 'Settings' }).getByText('Settings').click()
})
}
/** Find a "profile picture" settings section. */
export function locate(page: test.Page) {
return page.getByRole('heading').and(page.getByText('Profile Picture')).locator('..')
}
/** Find a "profile picture" input. */
export function locateInput(page: test.Page) {
return locate(page).locator('label')
}
}
export namespace organization {
/** Navigate so that the "organization" settings section is visible. */
export async function go(page: test.Page) {
await test.test.step('Go to "organization" settings section', async () => {
await locateUserMenu(page).click()
await page.getByRole('button', { name: 'Settings' }).getByText('Settings').click()
await settings.tab.organization.locate(page).click()
})
}
/** Find an "organization" settings section. */
export function locate(page: test.Page) {
return page.getByRole('heading').and(page.getByText('Organization')).locator('..')
}
/** Find a "name" input in the "organization" settings section. */
export function locateNameInput(page: test.Page) {
return locate(page).getByLabel('Organization display name')
}
/** Find an "email" input in the "organization" settings section. */
// eslint-disable-next-line @typescript-eslint/no-shadow
export function locateEmailInput(page: test.Page) {
return locate(page).getByLabel('Email')
}
/** Find an "website" input in the "organization" settings section. */
export function locateWebsiteInput(page: test.Page) {
return locate(page).getByLabel('Website')
}
/** Find an "location" input in the "organization" settings section. */
export function locateLocationInput(page: test.Page) {
return locate(page).getByLabel('Location')
}
}
export namespace organizationProfilePicture {
/** Navigate so that the "organization profile picture" settings section is visible. */
export async function go(page: test.Page) {
await test.test.step('Go to "organization profile picture" settings section', async () => {
await locateUserMenu(page).click()
await page.getByRole('button', { name: 'Settings' }).getByText('Settings').click()
await settings.tab.organization.locate(page).click()
})
}
/** Find an "organization profile picture" settings section. */
export function locate(page: test.Page) {
return page.getByRole('heading').and(page.getByText('Profile Picture')).locator('..')
}
/** Find a "profile picture" input. */
export function locateInput(page: test.Page) {
return locate(page).locator('label')
}
}
export namespace members {
/** Navigate so that the "members" settings section is visible. */
export async function go(page: test.Page, force = false) {
await test.test.step('Go to "members" settings section', async () => {
await locateUserMenu(page).click()
await page.getByRole('button', { name: 'Settings' }).getByText('Settings').click()
await settings.tab.members.locate(page).click({ force })
})
}
/** Find a "members" settings section. */
export function locate(page: test.Page) {
return page.getByRole('heading').and(page.getByText('Members')).locator('..')
}
/** Find all rows representing members of the current organization. */
export function locateMembersRows(page: test.Page) {
return locate(page).locator('tbody').getByRole('row')
}
}
}
// ===============================
// === Visual layout utilities ===
// ===============================
@ -614,7 +625,7 @@ export function getAssetRowLeftPx(locator: test.Locator) {
}
// ===================================
// === expect functions for themes ===
// === Expect functions for themes ===
// ===================================
/** A test assertion to confirm that the element has the class `selected`. */
@ -624,45 +635,69 @@ export async function expectClassSelected(locator: test.Locator) {
})
}
/** A test assertion to confirm that the element has the class `selected`. */
export async function expectNotTransparent(locator: test.Locator) {
await test.test.step('expect.not.transparent', async () => {
await test.expect
.poll(() => locator.evaluate(element => getComputedStyle(element).opacity))
.not.toBe('0')
// ==============================
// === Other expect functions ===
// ==============================
/** A test assertion to confirm that the element is fully transparent. */
export async function expectOpacity0(locator: test.Locator) {
await test.test.step('Expect `opacity: 0`', async () => {
await test
.expect(async () => {
test.expect(await locator.evaluate(el => getComputedStyle(el).opacity)).toBe('0')
})
.toPass()
})
}
/** A test assertion to confirm that the element has the class `selected`. */
export async function expectTransparent(locator: test.Locator) {
await test.test.step('expect.transparent', async () => {
await test.expect
.poll(() => locator.evaluate(element => getComputedStyle(element).opacity))
.toBe('0')
/** A test assertion to confirm that the element is not fully transparent. */
export async function expectNotOpacity0(locator: test.Locator) {
await test.test.step('Expect not `opacity: 0`', async () => {
await test
.expect(async () => {
test.expect(await locator.evaluate(el => getComputedStyle(el).opacity)).not.toBe('0')
})
.toPass()
})
}
// ============================
// === expectPlaceholderRow ===
// ============================
/** A test assertion to confirm that there is only one row visible, and that row is the
* placeholder row displayed when there are no assets to show. */
export async function expectPlaceholderRow(page: test.Page) {
const assetRows = locateAssetRows(page)
await test.test.step('Expect placeholder row', async () => {
await test.expect(assetRows).toHaveCount(1)
await test.expect(assetRows).toHaveText(/You have no files/)
/** A test assertion to confirm that the element is onscreen. */
export async function expectOnScreen(locator: test.Locator) {
await test.test.step('Expect to be onscreen', async () => {
await test
.expect(async () => {
const pageBounds = await locator.evaluate(() => document.body.getBoundingClientRect())
const bounds = await locator.evaluate(el => el.getBoundingClientRect())
test
.expect(
bounds.left < pageBounds.right &&
bounds.right > pageBounds.left &&
bounds.top < pageBounds.bottom &&
bounds.bottom > pageBounds.top
)
.toBe(true)
})
.toPass()
})
}
/** A test assertion to confirm that there is only one row visible, and that row is the
* placeholder row displayed when there are no assets in Trash. */
export async function expectTrashPlaceholderRow(page: test.Page) {
const assetRows = locateAssetRows(page)
await test.test.step('Expect trash placeholder row', async () => {
await test.expect(assetRows).toHaveCount(1)
await test.expect(assetRows).toHaveText(/Your trash is empty/)
/** A test assertion to confirm that the element is onscreen. */
export async function expectNotOnScreen(locator: test.Locator) {
await test.test.step('Expect to not be onscreen', async () => {
await test
.expect(async () => {
const pageBounds = await locator.evaluate(() => document.body.getBoundingClientRect())
const bounds = await locator.evaluate(el => el.getBoundingClientRect())
test
.expect(
bounds.left >= pageBounds.right ||
bounds.right <= pageBounds.left ||
bounds.top >= pageBounds.bottom ||
bounds.bottom <= pageBounds.top
)
.toBe(true)
})
.toPass()
})
}
@ -671,7 +706,7 @@ export async function expectTrashPlaceholderRow(page: test.Page) {
// =======================
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
const ASSET_ROW_SAFE_POSITION = { x: 300, y: 16 }
export const ASSET_ROW_SAFE_POSITION = { x: 300, y: 16 }
/** Click an asset row. The center must not be clicked as that is the button for adding a label. */
export async function clickAssetRow(assetRow: test.Locator) {
@ -710,19 +745,21 @@ export async function modModifier(page: test.Page) {
/** Press a key, replacing the text `Mod` with `Meta` (`Cmd`) on macOS, and `Control`
* on all other platforms. */
export async function press(page: test.Page, keyOrShortcut: string) {
if (/\bMod\b|\bDelete\b/.test(keyOrShortcut)) {
let userAgent = ''
await test.test.step('Detect browser OS', async () => {
userAgent = await page.evaluate(() => navigator.userAgent)
})
const isMacOS = /\bMac OS\b/i.test(userAgent)
const ctrlKey = isMacOS ? 'Meta' : 'Control'
const deleteKey = isMacOS ? 'Backspace' : 'Delete'
const shortcut = keyOrShortcut.replace(/\bMod\b/, ctrlKey).replace(/\bDelete\b/, deleteKey)
await test.test.step(`Press '${shortcut}'`, () => page.keyboard.press(shortcut))
} else {
await page.keyboard.press(keyOrShortcut)
}
await test.test.step(`Press '${keyOrShortcut}'`, async () => {
if (/\bMod\b|\bDelete\b/.test(keyOrShortcut)) {
let userAgent = ''
await test.test.step('Detect browser OS', async () => {
userAgent = await page.evaluate(() => navigator.userAgent)
})
const isMacOS = /\bMac OS\b/i.test(userAgent)
const ctrlKey = isMacOS ? 'Meta' : 'Control'
const deleteKey = isMacOS ? 'Backspace' : 'Delete'
const shortcut = keyOrShortcut.replace(/\bMod\b/, ctrlKey).replace(/\bDelete\b/, deleteKey)
await page.keyboard.press(shortcut)
} else {
await page.keyboard.press(keyOrShortcut)
}
})
}
// =============
@ -733,15 +770,55 @@ export async function press(page: test.Page, keyOrShortcut: string) {
// This syntax is required for Playwright to work properly.
// eslint-disable-next-line no-restricted-syntax
export async function login(
{ page }: MockParams,
{ page, setupAPI }: MockParams,
email = 'email@example.com',
password = VALID_PASSWORD,
first = true
) {
await test.test.step('Login', async () => {
await page.goto('/')
await locateEmailInput(page).fill(email)
await locatePasswordInput(page).fill(password)
await locateLoginButton(page).click()
await test.expect(page.getByText('Logging in to Enso...')).not.toBeVisible()
if (first) {
await passTermsAndConditionsDialog({ page, setupAPI })
await test.expect(page.getByText('Logging in to Enso...')).not.toBeVisible()
}
})
}
// ==============
// === reload ===
// ==============
/** Reload. */
// This syntax is required for Playwright to work properly.
// eslint-disable-next-line no-restricted-syntax
export async function reload({ page }: MockParams) {
await test.test.step('Reload', async () => {
await page.reload()
await test.expect(page.getByText('Logging in to Enso...')).not.toBeVisible()
})
}
// =============
// === relog ===
// =============
/** Logout and then login again. */
// This syntax is required for Playwright to work properly.
// eslint-disable-next-line no-restricted-syntax
export async function relog(
{ page, setupAPI }: MockParams,
email = 'email@example.com',
password = VALID_PASSWORD
) {
await page.goto('/')
await locateEmailInput(page).fill(email)
await locatePasswordInput(page).fill(password)
await locateLoginButton(page).click()
await locateToastCloseButton(page).click()
await test.test.step('Relog', async () => {
await page.getByAltText('User Settings').locator('visible=true').click()
await page.getByRole('button', { name: 'Logout' }).getByText('Logout').click()
await login({ page, setupAPI }, email, password, false)
})
}
// ================
@ -754,6 +831,7 @@ const MOCK_DATE = Number(new Date('01/23/45 01:23:45'))
/** Parameters for {@link mockDate}. */
interface MockParams {
readonly page: test.Page
readonly setupAPI?: apiModule.SetupAPI | undefined
}
/** Replace `Date` with a version that returns a fixed time. */
@ -761,7 +839,8 @@ interface MockParams {
// eslint-disable-next-line no-restricted-syntax
async function mockDate({ page }: MockParams) {
// https://github.com/microsoft/playwright/issues/6347#issuecomment-1085850728
await page.addInitScript(`{
await test.test.step('Mock Date', async () => {
await page.addInitScript(`{
Date = class extends Date {
constructor(...args) {
if (args.length === 0) {
@ -775,22 +854,15 @@ async function mockDate({ page }: MockParams) {
const __DateNow = Date.now;
Date.now = () => __DateNow() + __DateNowOffset;
}`)
})
}
// ========================
// === mockIDEContainer ===
// ========================
/** Make the IDE container have a non-zero size. */
// This syntax is required for Playwright to work properly.
// eslint-disable-next-line no-restricted-syntax
export async function mockIDEContainer({ page }: MockParams) {
await page.evaluate(() => {
const ideContainer = document.getElementById('app')
if (ideContainer) {
ideContainer.style.height = '100vh'
ideContainer.style.width = '100vw'
}
/** Pass the Terms and conditions dialog. */
export async function passTermsAndConditionsDialog({ page }: MockParams) {
await test.test.step('Accept Terms and Conditions', async () => {
await page.waitForSelector('#terms-of-service-modal')
await page.getByRole('checkbox').click()
await page.getByRole('button', { name: 'Accept' }).click()
})
}
@ -809,11 +881,11 @@ export const mockApi = apiModule.mockApi
/** Set up all mocks, without logging in. */
// This syntax is required for Playwright to work properly.
// eslint-disable-next-line no-restricted-syntax
export async function mockAll({ page }: MockParams) {
const api = await mockApi({ page })
await mockDate({ page })
await mockIDEContainer({ page })
return { api }
export function mockAll({ page, setupAPI }: MockParams) {
return new LoginPageActions(page).step('Execute all mocks', async () => {
await mockApi({ page, setupAPI })
await mockDate({ page, setupAPI })
})
}
// =======================
@ -823,11 +895,28 @@ export async function mockAll({ page }: MockParams) {
/** Set up all mocks, and log in with dummy credentials. */
// This syntax is required for Playwright to work properly.
// eslint-disable-next-line no-restricted-syntax
export async function mockAllAndLogin({ page }: MockParams) {
const mocks = await mockAll({ page })
await login({ page })
// This MUST run after login, otherwise the element's styles are reset when the browser
// is navigated to another page.
await mockIDEContainer({ page })
return mocks
export function mockAllAndLogin({ page, setupAPI }: MockParams) {
return new DrivePageActions(page)
.step('Execute all mocks', async () => {
await mockApi({ page, setupAPI })
await mockDate({ page, setupAPI })
})
.do(thePage => login({ page: thePage, setupAPI }))
}
// ===================================
// === mockAllAndLoginAndExposeAPI ===
// ===================================
/** Set up all mocks, and log in with dummy credentials.
* @deprecated Prefer {@link mockAllAndLogin}. */
// This syntax is required for Playwright to work properly.
// eslint-disable-next-line no-restricted-syntax
export async function mockAllAndLoginAndExposeAPI({ page, setupAPI }: MockParams) {
return await test.test.step('Execute all mocks and login', async () => {
const api = await mockApi({ page, setupAPI })
await mockDate({ page, setupAPI })
await login({ page, setupAPI })
return api
})
}

View File

@ -0,0 +1,158 @@
/** @file The base class from which all `Actions` classes are derived. */
import * as test from '@playwright/test'
import type * as inputBindings from '#/utilities/inputBindings'
import * as actions from '../actions'
// ====================
// === PageCallback ===
// ====================
/** A callback that performs actions on a {@link test.Page}. */
export interface PageCallback {
(input: test.Page): Promise<void> | void
}
// =======================
// === LocatorCallback ===
// =======================
/** A callback that performs actions on a {@link test.Locator}. */
export interface LocatorCallback {
(input: test.Locator): Promise<void> | void
}
// ===================
// === BaseActions ===
// ===================
/** The base class from which all `Actions` classes are derived.
* It contains method common to all `Actions` subclasses.
* This is a [`thenable`], so it can be used as if it was a {@link Promise}.
*
* [`thenable`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables
*/
export default class BaseActions implements Promise<void> {
/** Create a {@link BaseActions}. */
constructor(
protected readonly page: test.Page,
private readonly promise = Promise.resolve()
) {}
/** Get the string name of the class of this instance. Required for this class to implement
* {@link Promise}. */
get [Symbol.toStringTag]() {
return this.constructor.name
}
/** Press a key, replacing the text `Mod` with `Meta` (`Cmd`) on macOS, and `Control`
* on all other platforms. */
static press(page: test.Page, keyOrShortcut: string): Promise<void> {
return test.test.step(`Press '${keyOrShortcut}'`, async () => {
if (/\bMod\b|\bDelete\b/.test(keyOrShortcut)) {
let userAgent = ''
await test.test.step('Detect browser OS', async () => {
userAgent = await page.evaluate(() => navigator.userAgent)
})
const isMacOS = /\bMac OS\b/i.test(userAgent)
const ctrlKey = isMacOS ? 'Meta' : 'Control'
const deleteKey = isMacOS ? 'Backspace' : 'Delete'
const shortcut = keyOrShortcut.replace(/\bMod\b/, ctrlKey).replace(/\bDelete\b/, deleteKey)
await page.keyboard.press(shortcut)
} else {
await page.keyboard.press(keyOrShortcut)
}
})
}
/** Proxies the `then` method of the internal {@link Promise}. */
async then<T, E>(
// The following types are copied almost verbatim from the type definitions for `Promise`.
// eslint-disable-next-line no-restricted-syntax
onfulfilled?: (() => PromiseLike<T> | T) | null | undefined,
// eslint-disable-next-line no-restricted-syntax
onrejected?: ((reason: unknown) => E | PromiseLike<E>) | null | undefined
) {
return await this.promise.then(onfulfilled, onrejected)
}
/** Proxies the `catch` method of the internal {@link Promise}.
* This method is not required for this to be a `thenable`, but it is still useful
* to treat this class as a {@link Promise}. */
// The following types are copied almost verbatim from the type definitions for `Promise`.
// eslint-disable-next-line no-restricted-syntax
async catch<T>(onrejected?: ((reason: unknown) => PromiseLike<T> | T) | null | undefined) {
return await this.promise.catch(onrejected)
}
/** Proxies the `catch` method of the internal {@link Promise}.
* This method is not required for this to be a `thenable`, but it is still useful
* to treat this class as a {@link Promise}. */
async finally(onfinally?: (() => void) | null | undefined): Promise<void> {
await this.promise.finally(onfinally)
}
/** Return a {@link BaseActions} with the same {@link Promise} but a different type. */
into<
T extends new (page: test.Page, promise: Promise<void>, ...args: Args) => InstanceType<T>,
Args extends readonly unknown[],
>(clazz: T, ...args: Args): InstanceType<T> {
return new clazz(this.page, this.promise, ...args)
}
/** Perform an action on the current page. This should generally be avoided in favor of using
* specific methods; this is more or less an escape hatch used ONLY when the methods do not
* support desired functionality. */
do(callback: PageCallback): this {
// @ts-expect-error This is SAFE, but only when the constructor of this class has the exact
// same parameters as `BaseActions`.
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return new this.constructor(
this.page,
this.then(() => callback(this.page))
)
}
/** Perform an action on the current page. */
step(name: string, callback: PageCallback) {
return this.do(() => test.test.step(name, () => callback(this.page)))
}
/** Press a key, replacing the text `Mod` with `Meta` (`Cmd`) on macOS, and `Control`
* on all other platforms. */
press<Key extends string>(keyOrShortcut: inputBindings.AutocompleteKeybind<Key>) {
return this.do(page => BaseActions.press(page, keyOrShortcut))
}
/** Perform actions until a predicate passes. */
retry(
callback: (actions: this) => this,
predicate: (page: test.Page) => Promise<boolean>,
options: { retries?: number; delay?: number } = {}
) {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
const { retries = 3, delay = 1_000 } = options
return this.step('Perform actions with retries', async thePage => {
for (let i = 0; i < retries; i += 1) {
await callback(this)
if (await predicate(thePage)) {
// eslint-disable-next-line no-restricted-syntax
return
}
await thePage.waitForTimeout(delay)
}
throw new Error('This action did not succeed.')
})
}
/** Perform actions with the "Mod" modifier key pressed. */
withModPressed<R extends BaseActions>(callback: (actions: this) => R) {
return callback(
this.step('Press "Mod"', async page => {
await page.keyboard.down(await actions.modModifier(page))
})
).step('Release "Mod"', async page => {
await page.keyboard.up(await actions.modModifier(page))
})
}
}

View File

@ -0,0 +1,291 @@
/** @file Actions for the "drive" page. */
import * as test from 'playwright/test'
import * as actions from '../actions'
import type * as baseActions from './BaseActions'
import * as contextMenuActions from './contextMenuActions'
import EditorPageActions from './EditorPageActions'
import * as goToPageActions from './goToPageActions'
import NewDataLinkModalActions from './NewDataLinkModalActions'
import PageActions from './PageActions'
import StartModalActions from './StartModalActions'
// =================
// === Constants ===
// =================
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
const ASSET_ROW_SAFE_POSITION = { x: 300, y: 16 }
// =======================
// === locateAssetRows ===
// =======================
/** Find all assets table rows (if any). */
function locateAssetRows(page: test.Page) {
return actions.locateAssetsTable(page).locator('tbody').getByRole('row')
}
// ========================
// === DrivePageActions ===
// ========================
/** Actions for the "drive" page. */
export default class DrivePageActions extends PageActions {
/** Actions for navigating to another page. */
get goToPage(): Omit<goToPageActions.GoToPageActions, 'drive'> {
return goToPageActions.goToPageActions(this.step.bind(this))
}
/** Actions related to context menus. */
get contextMenu() {
return contextMenuActions.contextMenuActions(this.step.bind(this))
}
/** Switch to a different category. */
get goToCategory() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self: DrivePageActions = this
return {
/** Switch to the "cloud" category. */
cloud() {
return self.step('Go to "Cloud" category', page =>
page.getByRole('button', { name: 'Cloud' }).getByText('Cloud').click()
)
},
/** Switch to the "local" category. */
local() {
return self.step('Go to "Local" category', page =>
page.getByRole('button', { name: 'Local' }).getByText('Local').click()
)
},
/** Switch to the "recent" category. */
recent() {
return self.step('Go to "Recent" category', page =>
page.getByRole('button', { name: 'Recent' }).getByText('Recent').click()
)
},
/** Switch to the "trash" category. */
trash() {
return self.step('Go to "Trash" category', page =>
page.getByRole('button', { name: 'Trash' }).getByText('Trash').click()
)
},
}
}
/** Actions specific to the Drive table. */
get driveTable() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self: DrivePageActions = this
return {
/** Click the column heading for the "name" column to change its sort order. */
clickNameColumnHeading() {
return self.step('Click "name" column heading', page =>
page.getByLabel('Sort by name').or(page.getByLabel('Stop sorting by name')).click()
)
},
/** Click the column heading for the "modified" column to change its sort order. */
clickModifiedColumnHeading() {
return self.step('Click "modified" column heading', page =>
page
.getByLabel('Sort by modification date')
.or(page.getByLabel('Stop sorting by modification date'))
.click()
)
},
/** Click to select a specific row. */
clickRow(index: number) {
return self.step(`Click drive table row #${index}`, page =>
locateAssetRows(page).nth(index).click({ position: actions.ASSET_ROW_SAFE_POSITION })
)
},
/** Right click a specific row to bring up its context menu, or the context menu for multiple
* assets when right clicking on a selected asset when multiple assets are selected. */
rightClickRow(index: number) {
return self.step(`Right click drive table row #${index}`, page =>
locateAssetRows(page)
.nth(index)
.click({ button: 'right', position: actions.ASSET_ROW_SAFE_POSITION })
)
},
/** Double click a row. */
doubleClickRow(index: number) {
return self.step(`Double dlick drive table row #${index}`, page =>
locateAssetRows(page).nth(index).dblclick({ position: actions.ASSET_ROW_SAFE_POSITION })
)
},
/** Interact with the set of all rows in the Drive table. */
withRows(callback: baseActions.LocatorCallback) {
return self.step('Interact with drive table rows', async page => {
await callback(locateAssetRows(page))
})
},
/** Drag a row onto another row. */
dragRowToRow(from: number, to: number) {
return self.step(`Drag drive table row #${from} to row #${to}`, async page => {
const rows = locateAssetRows(page)
await rows.nth(from).dragTo(rows.nth(to), {
sourcePosition: ASSET_ROW_SAFE_POSITION,
targetPosition: ASSET_ROW_SAFE_POSITION,
})
})
},
/** Drag a row onto another row. */
dragRow(from: number, to: test.Locator, force?: boolean) {
return self.step(`Drag drive table row #${from} to custom locator`, page =>
locateAssetRows(page)
.nth(from)
.dragTo(to, {
sourcePosition: ASSET_ROW_SAFE_POSITION,
...(force == null ? {} : { force }),
})
)
},
/** A test assertion to confirm that there is only one row visible, and that row is the
* placeholder row displayed when there are no assets to show. */
expectPlaceholderRow() {
return self.step('Expect placeholder row', async page => {
const rows = locateAssetRows(page)
await test.expect(rows).toHaveCount(1)
await test.expect(rows).toHaveText(/You have no files/)
})
},
/** A test assertion to confirm that there is only one row visible, and that row is the
* placeholder row displayed when there are no assets in Trash. */
expectTrashPlaceholderRow() {
return self.step('Expect trash placeholder row', async page => {
const rows = locateAssetRows(page)
await test.expect(rows).toHaveCount(1)
await test.expect(rows).toHaveText(/Your trash is empty/)
})
},
/** Toggle a column's visibility. */
get toggleColumn() {
return {
/** Toggle visibility for the "modified" column. */
modified() {
return self.step('Expect trash placeholder row', page =>
page.getByAltText('Modified').click()
)
},
/** Toggle visibility for the "shared with" column. */
sharedWith() {
return self.step('Expect trash placeholder row', page =>
page.getByAltText('Shared With').click()
)
},
/** Toggle visibility for the "labels" column. */
labels() {
return self.step('Expect trash placeholder row', page =>
page.getByAltText('Labels').click()
)
},
/** Toggle visibility for the "accessed by projects" column. */
accessedByProjects() {
return self.step('Expect trash placeholder row', page =>
page.getByAltText('Accessed By Projects').click()
)
},
/** Toggle visibility for the "accessed data" column. */
accessedData() {
return self.step('Expect trash placeholder row', page =>
page.getByAltText('Accessed Data').click()
)
},
/** Toggle visibility for the "docs" column. */
docs() {
return self.step('Expect trash placeholder row', page =>
page.getByAltText('Docs').click()
)
},
}
},
}
}
/** Open the "start" modal. */
openStartModal() {
return this.step('Open "start" modal', page =>
page.getByText('Start with a template').click()
).into(StartModalActions)
}
/** Create a new empty project. */
newEmptyProject() {
return this.step('Create empty project', page =>
page.getByText('New Empty Project').click()
).into(EditorPageActions)
}
/** Interact with the drive view (the main container of this page). */
withDriveView(callback: baseActions.LocatorCallback) {
return this.step('Interact with drive view', page => callback(actions.locateDriveView(page)))
}
/** Create a new folder using the icon in the Drive Bar. */
createFolder() {
return this.step('Create folder', page =>
page.getByRole('button', { name: 'New Folder' }).click()
)
}
/** Upload a file using the icon in the Drive Bar. */
uploadFile(
name: string,
contents: WithImplicitCoercion<Uint8Array | string | readonly number[]>,
mimeType = 'text/plain'
) {
return this.step(`Upload file '${name}'`, async page => {
const fileChooserPromise = page.waitForEvent('filechooser')
await page.getByRole('button', { name: 'Import' }).click()
const fileChooser = await fileChooserPromise
await fileChooser.setFiles([{ name, buffer: Buffer.from(contents), mimeType }])
})
}
/** Create a new secret using the icon in the Drive Bar. */
createSecret(name: string, value: string) {
return this.step(`Create secret '${name}' = '${value}'`, async page => {
await actions.locateNewSecretIcon(page).click()
await actions.locateSecretNameInput(page).fill(name)
await actions.locateSecretValueInput(page).fill(value)
await actions.locateCreateButton(page).click()
})
}
/** Toggle the Asset Panel open or closed. */
toggleAssetPanel() {
return this.step('Toggle asset panel', page =>
page.getByLabel('Asset Panel').locator('visible=true').click()
)
}
/** Interact with the container element of the assets table. */
withAssetsTable(callback: baseActions.LocatorCallback) {
return this.step('Interact with drive table', async page => {
await callback(actions.locateAssetsTable(page))
})
}
/** Interact with the Asset Panel. */
withAssetPanel(callback: baseActions.LocatorCallback) {
return this.step('Interact with asset panel', async page => {
await callback(actions.locateAssetPanel(page))
})
}
/** Open the Data Link creation modal by clicking on the Data Link icon. */
openDataLinkModal() {
return this.step('Open "new data link" modal', page =>
page.getByRole('button', { name: 'New Datalink' }).click()
).into(NewDataLinkModalActions)
}
/** Interact with the context menus (the context menus MUST be visible). */
withContextMenus(callback: baseActions.LocatorCallback) {
return this.step('Interact with context menus', async page => {
await callback(actions.locateContextMenus(page))
})
}
}

View File

@ -0,0 +1,15 @@
/** @file Actions for the "editor" page. */
import * as goToPageActions from './goToPageActions'
import PageActions from './PageActions'
// =========================
// === EditorPageActions ===
// =========================
/** Actions for the "editor" page. */
export default class EditorPageActions extends PageActions {
/** Actions for navigating to another page. */
get goToPage(): Omit<goToPageActions.GoToPageActions, 'editor'> {
return goToPageActions.goToPageActions(this.step.bind(this))
}
}

View File

@ -0,0 +1,40 @@
/** @file Available actions for the login page. */
import * as test from '@playwright/test'
import * as actions from '../actions'
import BaseActions from './BaseActions'
import DrivePageActions from './DrivePageActions'
import SetUsernamePageActions from './SetUsernamePageActions'
// ========================
// === LoginPageActions ===
// ========================
/** Available actions for the login page. */
export default class LoginPageActions extends BaseActions {
/** Perform a successful login. */
login(email = 'email@example.com', password = actions.VALID_PASSWORD) {
return this.step('Login', () => this.loginInternal(email, password)).into(DrivePageActions)
}
/** Perform a login as a new user (a user that does not yet have a username). */
loginAsNewUser(email = 'email@example.com', password = actions.VALID_PASSWORD) {
return this.step('Login (as new user)', () => this.loginInternal(email, password)).into(
SetUsernamePageActions
)
}
/** Perform a failing login. */
loginThatShouldFail(email = 'email@example.com', password = actions.VALID_PASSWORD) {
return this.step('Login (should fail)', () => this.loginInternal(email, password))
}
/** Internal login logic shared between all public methods. */
private async loginInternal(email: string, password: string) {
await this.page.goto('/')
await this.page.getByPlaceholder('Enter your email').fill(email)
await this.page.getByPlaceholder('Enter your password').fill(password)
await this.page.getByRole('button', { name: 'Login', exact: true }).getByText('Login').click()
await test.expect(this.page.getByText('Logging in to Enso...')).not.toBeVisible()
}
}

View File

@ -0,0 +1,37 @@
/** @file Actions for a "new Data Link" modal. */
import type * as test from 'playwright/test'
import type * as baseActions from './BaseActions'
import BaseActions from './BaseActions'
import DrivePageActions from './DrivePageActions'
// ==============================
// === locateNewDataLinkModal ===
// ==============================
/** Locate the "new data link" modal. */
function locateNewDataLinkModal(page: test.Page) {
return page.getByRole('heading').and(page.getByText('Create Datalink')).locator('..')
}
// ===============================
// === NewDataLinkModalActions ===
// ===============================
/** Actions for a "new Data Link" modal. */
export default class NewDataLinkModalActions extends BaseActions {
/** Cancel creating the new Data Link (don't submit the form). */
cancel() {
return this.step('Cancel out of "new data link" modal', async () => {
await this.press('Escape')
}).into(DrivePageActions)
}
/** Interact with the "name" input - for example, to set the name using `.fill("")`. */
withNameInput(callback: baseActions.LocatorCallback) {
return this.step('Interact with "name" input', async page => {
const locator = locateNewDataLinkModal(page).getByLabel('Name')
await callback(locator)
})
}
}

View File

@ -0,0 +1,21 @@
/** @file Actions common to all pages. */
import BaseActions from './BaseActions'
import * as openUserMenuAction from './openUserMenuAction'
import * as userMenuActions from './userMenuActions'
// ===================
// === PageActions ===
// ===================
/** Actions common to all pages. */
export default class PageActions extends BaseActions {
/** Actions related to the User Menu. */
get userMenu() {
return userMenuActions.userMenuActions(this.step.bind(this))
}
/** Open the User Menu. */
openUserMenu() {
return openUserMenuAction.openUserMenuAction(this.step.bind(this))
}
}

View File

@ -0,0 +1,19 @@
/** @file Actions for the "set username" page. */
import * as actions from '../actions'
import BaseActions from './BaseActions'
import DrivePageActions from './DrivePageActions'
// ==============================
// === SetUsernamePageActions ===
// ==============================
/** Actions for the "set username" page. */
export default class SetUsernamePageActions extends BaseActions {
/** Set the userame for a new user that does not yet have a username. */
setUsername(username: string) {
return this.step(`Set username to '${username}'`, async page => {
await actions.locateUsernameInput(page).fill(username)
await actions.locateSetUsernameButton(page).click()
}).into(DrivePageActions)
}
}

View File

@ -0,0 +1,16 @@
/** @file Actions for the "settings" page. */
import * as goToPageActions from './goToPageActions'
import PageActions from './PageActions'
// ===========================
// === SettingsPageActions ===
// ===========================
// TODO: split settings page actions into different classes for each settings tab.
/** Actions for the "settings" page. */
export default class SettingsPageActions extends PageActions {
/** Actions for navigating to another page. */
get goToPage(): Omit<goToPageActions.GoToPageActions, 'drive'> {
return goToPageActions.goToPageActions(this.step.bind(this))
}
}

View File

@ -0,0 +1,29 @@
/** @file Actions for the "home" page. */
import * as actions from '../actions'
import BaseActions from './BaseActions'
import DrivePageActions from './DrivePageActions'
import EditorPageActions from './EditorPageActions'
// =========================
// === StartModalActions ===
// =========================
/** Actions for the "start" modal. */
export default class StartModalActions extends BaseActions {
/** Close this modal and go back to the Drive page. */
close() {
return this.step('Close "start" modal', page => page.getByLabel('Close').click()).into(
DrivePageActions
)
}
/** Create a project from the template at the given index. */
createProjectFromTemplate(index: number) {
return this.step(`Create project from template #${index}`, page =>
actions
.locateSamples(page)
.nth(index + 1)
.click()
).into(EditorPageActions)
}
}

View File

@ -0,0 +1,139 @@
/** @file Actions for the context menu. */
import type * as baseActions from './BaseActions'
import type BaseActions from './BaseActions'
import EditorPageActions from './EditorPageActions'
// ==========================
// === ContextMenuActions ===
// ==========================
/** Actions for the context menu. */
export interface ContextMenuActions<T extends BaseActions> {
readonly open: () => T
readonly uploadToCloud: () => T
readonly rename: () => T
readonly snapshot: () => T
readonly moveToTrash: () => T
readonly moveAllToTrash: () => T
readonly restoreFromTrash: () => T
readonly restoreAllFromTrash: () => T
readonly share: () => T
readonly label: () => T
readonly duplicate: () => T
readonly duplicateProject: () => EditorPageActions
readonly copy: () => T
readonly cut: () => T
readonly paste: () => T
readonly copyAsPath: () => T
readonly download: () => T
readonly uploadFiles: () => T
readonly newFolder: () => T
readonly newSecret: () => T
readonly newDataLink: () => T
}
// ==========================
// === contextMenuActions ===
// ==========================
/** Generate actions for the context menu. */
export function contextMenuActions<T extends BaseActions>(
step: (name: string, callback: baseActions.PageCallback) => T
): ContextMenuActions<T> {
return {
open: () =>
step('Open (context menu)', page =>
page.getByRole('button', { name: 'Open' }).getByText('Open').click()
),
uploadToCloud: () =>
step('Upload to cloud (context menu)', page =>
page.getByRole('button', { name: 'Upload To Cloud' }).getByText('Upload To Cloud').click()
),
rename: () =>
step('Rename (context menu)', page =>
page.getByRole('button', { name: 'Rename' }).getByText('Rename').click()
),
snapshot: () =>
step('Snapshot (context menu)', page =>
page.getByRole('button', { name: 'Snapshot' }).getByText('Snapshot').click()
),
moveToTrash: () =>
step('Move to trash (context menu)', page =>
page.getByRole('button', { name: 'Move To Trash' }).getByText('Move To Trash').click()
),
moveAllToTrash: () =>
step('Move all to trash (context menu)', page =>
page
.getByRole('button', { name: 'Move All To Trash' })
.getByText('Move All To Trash')
.click()
),
restoreFromTrash: () =>
step('Restore from trash (context menu)', page =>
page
.getByRole('button', { name: 'Restore From Trash' })
.getByText('Restore From Trash')
.click()
),
restoreAllFromTrash: () =>
step('Restore all from trash (context menu)', page =>
page
.getByRole('button', { name: 'Restore All From Trash' })
.getByText('Restore All From Trash')
.click()
),
share: () =>
step('Share (context menu)', page =>
page.getByRole('button', { name: 'Share' }).getByText('Share').click()
),
label: () =>
step('Label (context menu)', page =>
page.getByRole('button', { name: 'Label' }).getByText('Label').click()
),
duplicate: () =>
step('Duplicate (context menu)', page =>
page.getByRole('button', { name: 'Duplicate' }).getByText('Duplicate').click()
),
duplicateProject: () =>
step('Duplicate project (context menu)', page =>
page.getByRole('button', { name: 'Duplicate' }).getByText('Duplicate').click()
).into(EditorPageActions),
copy: () =>
step('Copy (context menu)', page =>
page.getByRole('button', { name: 'Copy' }).getByText('Copy', { exact: true }).click()
),
cut: () =>
step('Cut (context menu)', page =>
page.getByRole('button', { name: 'Cut' }).getByText('Cut').click()
),
paste: () =>
step('Paste (context menu)', page =>
page.getByRole('button', { name: 'Paste' }).getByText('Paste').click()
),
copyAsPath: () =>
step('Copy as path (context menu)', page =>
page.getByRole('button', { name: 'Copy As Path' }).getByText('Copy As Path').click()
),
download: () =>
step('Download (context menu)', page =>
page.getByRole('button', { name: 'Download' }).getByText('Download').click()
),
// TODO: Specify the files in parameters.
uploadFiles: () =>
step('Upload files (context menu)', page =>
page.getByRole('button', { name: 'Upload Files' }).getByText('Upload Files').click()
),
newFolder: () =>
step('New folder (context menu)', page =>
page.getByRole('button', { name: 'New Folder' }).getByText('New Folder').click()
),
newSecret: () =>
step('New secret (context menu)', page =>
page.getByRole('button', { name: 'New Secret' }).getByText('New Secret').click()
),
newDataLink: () =>
step('New Data Link (context menu)', page =>
page.getByRole('button', { name: 'New Data Link' }).getByText('New Data Link').click()
),
}
}

View File

@ -0,0 +1,44 @@
/** @file Actions for going to a different page. */
import type * as baseActions from './BaseActions'
import BaseActions from './BaseActions'
import DrivePageActions from './DrivePageActions'
import EditorPageActions from './EditorPageActions'
import SettingsPageActions from './SettingsPageActions'
// =======================
// === GoToPageActions ===
// =======================
/** Actions for going to a different page. */
export interface GoToPageActions {
readonly drive: () => DrivePageActions
readonly editor: () => EditorPageActions
readonly settings: () => SettingsPageActions
}
// =======================
// === goToPageActions ===
// =======================
/** Generate actions for going to a different page. */
export function goToPageActions(
step: (name: string, callback: baseActions.PageCallback) => BaseActions
): GoToPageActions {
return {
drive: () =>
step('Go to "Data Catalog" page', page =>
page
.getByRole('tab')
.filter({ has: page.getByText('Data Catalog') })
.click()
).into(DrivePageActions),
editor: () =>
step('Go to "Spatial Analysis" page', page =>
page.getByTestId('editor-tab-button').click()
).into(EditorPageActions),
settings: () =>
step('Go to "settings" page', page => BaseActions.press(page, 'Mod+,')).into(
SettingsPageActions
),
}
}

View File

@ -0,0 +1,16 @@
/** @file An action to open the User Menu. */
import type * as baseActions from './BaseActions'
import type BaseActions from './BaseActions'
// ==========================
// === openUserMenuAction ===
// ==========================
/** An action to open the User Menu. */
export function openUserMenuAction<T extends BaseActions>(
step: (name: string, callback: baseActions.PageCallback) => T
) {
return step('Open user menu', page =>
page.getByAltText('User Settings').locator('visible=true').click()
)
}

View File

@ -0,0 +1,49 @@
/** @file Actions for the user menu. */
import type * as test from 'playwright/test'
import type * as baseActions from './BaseActions'
import type BaseActions from './BaseActions'
import LoginPageActions from './LoginPageActions'
import SettingsPageActions from './SettingsPageActions'
// =======================
// === UserMenuActions ===
// =======================
/** Actions for the user menu. */
export interface UserMenuActions<T extends BaseActions> {
readonly downloadApp: (callback: (download: test.Download) => Promise<void> | void) => T
readonly settings: () => SettingsPageActions
readonly logout: () => LoginPageActions
readonly goToLoginPage: () => LoginPageActions
}
// =======================
// === userMenuActions ===
// =======================
/** Generate actions for the user menu. */
export function userMenuActions<T extends BaseActions>(
step: (name: string, callback: baseActions.PageCallback) => T
): UserMenuActions<T> {
return {
downloadApp: (callback: (download: test.Download) => Promise<void> | void) =>
step('Download app (user menu)', async page => {
const downloadPromise = page.waitForEvent('download')
await page.getByRole('button', { name: 'Download App' }).getByText('Download App').click()
await callback(await downloadPromise)
}),
settings: () =>
step('Go to Settings (user menu)', async page => {
await page.getByRole('button', { name: 'Settings' }).getByText('Settings').click()
}).into(SettingsPageActions),
logout: () =>
step('Logout (user menu)', page =>
page.getByRole('button', { name: 'Logout' }).getByText('Logout').click()
).into(LoginPageActions),
goToLoginPage: () =>
step('Login (user menu)', page =>
page.getByRole('button', { name: 'Login', exact: true }).getByText('Login').click()
).into(LoginPageActions),
}
}

911
app/dashboard/e2e/api.ts Normal file
View File

@ -0,0 +1,911 @@
/** @file The mock API. */
import * as test from '@playwright/test'
import * as backend from '#/services/Backend'
import type * as remoteBackend from '#/services/RemoteBackend'
import * as remoteBackendPaths from '#/services/remoteBackendPaths'
import * as dateTime from '#/utilities/dateTime'
import * as object from '#/utilities/object'
import * as permissions from '#/utilities/permissions'
import * as uniqueString from '#/utilities/uniqueString'
import * as actions from './actions'
// =================
// === Constants ===
// =================
/** The HTTP status code representing a response with an empty body. */
const HTTP_STATUS_NO_CONTENT = 204
/** The HTTP status code representing a bad request. */
const HTTP_STATUS_BAD_REQUEST = 400
/** The HTTP status code representing a URL that does not exist. */
const HTTP_STATUS_NOT_FOUND = 404
/** An asset ID that is a path glob. */
const GLOB_ASSET_ID: backend.AssetId = backend.DirectoryId('*')
/** A directory ID that is a path glob. */
const GLOB_DIRECTORY_ID = backend.DirectoryId('*')
/** A project ID that is a path glob. */
const GLOB_PROJECT_ID = backend.ProjectId('*')
/** A tag ID that is a path glob. */
const GLOB_TAG_ID = backend.TagId('*')
/* eslint-enable no-restricted-syntax */
const BASE_URL = 'https://mock/'
// ===============
// === mockApi ===
// ===============
/** Parameters for {@link mockApi}. */
export interface MockParams {
readonly page: test.Page
readonly setupAPI?: SetupAPI | null | undefined
}
/**
* Setup function for the mock API.
* use it to setup the mock API with custom handlers.
*/
export interface SetupAPI {
(api: Awaited<ReturnType<typeof mockApi>>): Promise<void> | void
}
/** The return type of {@link mockApi}. */
export interface MockApi extends Awaited<ReturnType<typeof mockApiInternal>> {}
// This is a function, even though it does not contain function syntax.
// eslint-disable-next-line no-restricted-syntax
export const mockApi: (params: MockParams) => Promise<MockApi> = mockApiInternal
/** Add route handlers for the mock API to a page. */
// This syntax is required for Playwright to work properly.
// eslint-disable-next-line no-restricted-syntax
async function mockApiInternal({ page, setupAPI }: MockParams) {
// eslint-disable-next-line no-restricted-syntax
const defaultEmail = 'email@example.com' as backend.EmailAddress
const defaultUsername = 'user name'
const defaultPassword = actions.VALID_PASSWORD
const defaultOrganizationId = backend.OrganizationId('organization-placeholder id')
const defaultOrganizationName = 'organization name'
const defaultUserId = backend.UserId('user-placeholder id')
const defaultDirectoryId = backend.DirectoryId('directory-placeholder id')
const defaultUser: backend.User = {
email: defaultEmail,
name: defaultUsername,
organizationId: defaultOrganizationId,
userId: defaultUserId,
isEnabled: true,
rootDirectoryId: defaultDirectoryId,
userGroups: null,
plan: backend.Plan.enterprise,
}
const defaultOrganization: backend.OrganizationInfo = {
id: defaultOrganizationId,
name: defaultOrganizationName,
address: null,
email: null,
picture: null,
website: null,
subscription: {},
}
let isOnline = true
let currentUser: backend.User | null = defaultUser
let currentProfilePicture: string | null = null
let currentPassword = defaultPassword
let currentOrganization: backend.OrganizationInfo | null = defaultOrganization
let currentOrganizationProfilePicture: string | null = null
const assetMap = new Map<backend.AssetId, backend.AnyAsset>()
const deletedAssets = new Set<backend.AssetId>()
const assets: backend.AnyAsset[] = []
const labels: backend.Label[] = []
const labelsByValue = new Map<backend.LabelName, backend.Label>()
const labelMap = new Map<backend.TagId, backend.Label>()
const users: backend.User[] = [defaultUser]
const usersMap = new Map<backend.UserId, backend.User>()
const userGroups: backend.UserGroupInfo[] = []
usersMap.set(defaultUser.userId, defaultUser)
const addAsset = <T extends backend.AnyAsset>(asset: T) => {
assets.push(asset)
assetMap.set(asset.id, asset)
return asset
}
const deleteAsset = (assetId: backend.AssetId) => {
const alreadyDeleted = deletedAssets.has(assetId)
deletedAssets.add(assetId)
return !alreadyDeleted
}
const undeleteAsset = (assetId: backend.AssetId) => {
const wasDeleted = deletedAssets.has(assetId)
deletedAssets.delete(assetId)
return wasDeleted
}
const createDirectory = (
title: string,
rest: Partial<backend.DirectoryAsset> = {}
): backend.DirectoryAsset =>
object.merge(
{
type: backend.AssetType.directory,
id: backend.DirectoryId('directory-' + uniqueString.uniqueString()),
projectState: null,
title,
modifiedAt: dateTime.toRfc3339(new Date()),
description: null,
labels: [],
parentId: defaultDirectoryId,
permissions: [],
},
rest
)
const createProject = (
title: string,
rest: Partial<backend.ProjectAsset> = {}
): backend.ProjectAsset =>
object.merge(
{
type: backend.AssetType.project,
id: backend.ProjectId('project-' + uniqueString.uniqueString()),
projectState: {
type: backend.ProjectState.closed,
volumeId: '',
},
title,
modifiedAt: dateTime.toRfc3339(new Date()),
description: null,
labels: [],
parentId: defaultDirectoryId,
permissions: [],
},
rest
)
const createFile = (title: string, rest: Partial<backend.FileAsset> = {}): backend.FileAsset =>
object.merge(
{
type: backend.AssetType.file,
id: backend.FileId('file-' + uniqueString.uniqueString()),
projectState: null,
title,
modifiedAt: dateTime.toRfc3339(new Date()),
description: null,
labels: [],
parentId: defaultDirectoryId,
permissions: [],
},
rest
)
const createSecret = (
title: string,
rest: Partial<backend.SecretAsset> = {}
): backend.SecretAsset =>
object.merge(
{
type: backend.AssetType.secret,
id: backend.SecretId('secret-' + uniqueString.uniqueString()),
projectState: null,
title,
modifiedAt: dateTime.toRfc3339(new Date()),
description: null,
labels: [],
parentId: defaultDirectoryId,
permissions: [],
},
rest
)
const createLabel = (value: string, color: backend.LChColor): backend.Label => ({
id: backend.TagId('tag-' + uniqueString.uniqueString()),
value: backend.LabelName(value),
color,
})
const addDirectory = (title: string, rest?: Partial<backend.DirectoryAsset>) => {
return addAsset(createDirectory(title, rest))
}
const addProject = (title: string, rest?: Partial<backend.ProjectAsset>) => {
return addAsset(createProject(title, rest))
}
const addFile = (title: string, rest?: Partial<backend.FileAsset>) => {
return addAsset(createFile(title, rest))
}
const addSecret = (title: string, rest?: Partial<backend.SecretAsset>) => {
return addAsset(createSecret(title, rest))
}
const addLabel = (value: string, color: backend.LChColor) => {
const label = createLabel(value, color)
labels.push(label)
labelsByValue.set(label.value, label)
labelMap.set(label.id, label)
return label
}
const setLabels = (id: backend.AssetId, newLabels: backend.LabelName[]) => {
const ids = new Set<backend.AssetId>([id])
for (const [innerId, asset] of assetMap) {
if (ids.has(asset.parentId)) {
ids.add(innerId)
}
}
for (const innerId of ids) {
const asset = assetMap.get(innerId)
if (asset != null) {
object.unsafeMutable(asset).labels = newLabels
}
}
}
const addUser = (name: string, rest: Partial<backend.User> = {}) => {
const organizationId = currentOrganization?.id ?? defaultOrganizationId
const user: backend.User = {
userId: backend.UserId(`user-${uniqueString.uniqueString()}`),
name,
email: backend.EmailAddress(`${name}@example.org`),
organizationId,
rootDirectoryId: backend.DirectoryId(organizationId.replace(/^organization-/, 'directory-')),
isEnabled: true,
userGroups: null,
plan: backend.Plan.enterprise,
...rest,
}
users.push(user)
usersMap.set(user.userId, user)
return user
}
const deleteUser = (userId: backend.UserId) => {
usersMap.delete(userId)
const index = users.findIndex(user => user.userId === userId)
if (index === -1) {
return false
} else {
users.splice(index, 1)
return true
}
}
const addUserGroup = (name: string, rest: Partial<backend.UserGroupInfo>) => {
const userGroup: backend.UserGroupInfo = {
id: backend.UserGroupId(`usergroup-${uniqueString.uniqueString()}`),
groupName: name,
organizationId: currentOrganization?.id ?? defaultOrganizationId,
...rest,
}
userGroups.push(userGroup)
return userGroup
}
const deleteUserGroup = (userGroupId: backend.UserGroupId) => {
const index = userGroups.findIndex(userGroup => userGroup.id === userGroupId)
if (index === -1) {
return false
} else {
users.splice(index, 1)
return true
}
}
// addPermission,
// deletePermission,
// addUserGroupToUser,
// deleteUserGroupFromUser,
const addUserGroupToUser = (userId: backend.UserId, userGroupId: backend.UserGroupId) => {
const user = usersMap.get(userId)
if (user == null || user.userGroups?.includes(userGroupId) === true) {
// The user does not exist, or they are already in this group.
return false
} else {
const newUserGroups = object.unsafeMutable(user.userGroups ?? [])
newUserGroups.push(userGroupId)
object.unsafeMutable(user).userGroups = newUserGroups
return true
}
}
const removeUserGroupFromUser = (userId: backend.UserId, userGroupId: backend.UserGroupId) => {
const user = usersMap.get(userId)
if (user?.userGroups?.includes(userGroupId) !== true) {
// The user does not exist, or they are already not in this group.
return false
} else {
object.unsafeMutable(user.userGroups).splice(user.userGroups.indexOf(userGroupId), 1)
return true
}
}
await test.test.step('Mock API', async () => {
const method =
(theMethod: string) =>
async (url: string, callback: (route: test.Route, request: test.Request) => unknown) => {
await page.route(BASE_URL + url, async (route, request) => {
if (request.method() !== theMethod) {
await route.fallback()
} else {
const result = await callback(route, request)
// `null` counts as a JSON value that we will want to return.
// eslint-disable-next-line no-restricted-syntax
if (result !== undefined) {
await route.fulfill({ json: result })
}
}
})
}
const get = method('GET')
const put = method('PUT')
const post = method('POST')
const patch = method('PATCH')
// eslint-disable-next-line @typescript-eslint/naming-convention
const delete_ = method('DELETE')
await page.route('https://cdn.enso.org/**', route => route.fulfill())
await page.route('https://www.google-analytics.com/**', route => route.fulfill())
await page.route('https://www.googletagmanager.com/gtag/js*', route =>
route.fulfill({ contentType: 'text/javascript', body: 'export {};' })
)
const isActuallyOnline = await page.evaluate(() => navigator.onLine)
if (!isActuallyOnline) {
await page.route('https://fonts.googleapis.com/*', route => route.abort())
}
await page.route(BASE_URL + '**', (_route, request) => {
throw new Error(`Missing route handler for '${request.url().replace(BASE_URL, '')}'.`)
})
// === Mock Cognito endpoints ===
await page.route('https://mock-cognito.com/change-password', async (route, request) => {
if (request.method() !== 'POST') {
await route.fallback()
} else {
/** The type for the JSON request payload for this endpoint. */
interface Body {
readonly oldPassword: string
readonly newPassword: string
}
// The type of the body sent by this app is statically known.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const body: Body = await request.postDataJSON()
if (body.oldPassword === currentPassword) {
currentPassword = body.newPassword
await route.fulfill({ status: HTTP_STATUS_NO_CONTENT })
} else {
await route.fulfill({ status: HTTP_STATUS_BAD_REQUEST })
}
}
})
// === Endpoints returning arrays ===
await get(remoteBackendPaths.LIST_DIRECTORY_PATH + '*', (_route, request) => {
/** The type for the search query for this endpoint. */
interface Query {
/* eslint-disable @typescript-eslint/naming-convention */
readonly parent_id?: string
readonly filter_by?: backend.FilterBy
readonly labels?: backend.LabelName[]
readonly recent_projects?: boolean
/* eslint-enable @typescript-eslint/naming-convention */
}
// The type of the body sent by this app is statically known.
// eslint-disable-next-line no-restricted-syntax
const body = Object.fromEntries(
new URL(request.url()).searchParams.entries()
) as unknown as Query
const parentId = body.parent_id ?? defaultDirectoryId
let filteredAssets = assets.filter(asset => asset.parentId === parentId)
// This lint rule is broken; there is clearly a case for `undefined` below.
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
switch (body.filter_by) {
case backend.FilterBy.active: {
filteredAssets = filteredAssets.filter(asset => !deletedAssets.has(asset.id))
break
}
case backend.FilterBy.trashed: {
filteredAssets = filteredAssets.filter(asset => deletedAssets.has(asset.id))
break
}
case backend.FilterBy.recent: {
filteredAssets = assets
.filter(asset => !deletedAssets.has(asset.id))
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
.slice(0, 10)
break
}
case backend.FilterBy.all:
case null: {
// do nothing
break
}
// eslint-disable-next-line no-restricted-syntax
case undefined: {
// do nothing
break
}
}
filteredAssets.sort(
(a, b) => backend.ASSET_TYPE_ORDER[a.type] - backend.ASSET_TYPE_ORDER[b.type]
)
const json: remoteBackend.ListDirectoryResponseBody = { assets: filteredAssets }
return json
})
await get(
remoteBackendPaths.LIST_FILES_PATH + '*',
() => ({ files: [] }) satisfies remoteBackend.ListFilesResponseBody
)
await get(
remoteBackendPaths.LIST_PROJECTS_PATH + '*',
() => ({ projects: [] }) satisfies remoteBackend.ListProjectsResponseBody
)
await get(
remoteBackendPaths.LIST_SECRETS_PATH + '*',
() => ({ secrets: [] }) satisfies remoteBackend.ListSecretsResponseBody
)
await get(
remoteBackendPaths.LIST_TAGS_PATH + '*',
() => ({ tags: labels }) satisfies remoteBackend.ListTagsResponseBody
)
await get(remoteBackendPaths.LIST_USERS_PATH + '*', async route => {
if (currentUser != null) {
return { users } satisfies remoteBackend.ListUsersResponseBody
} else {
await route.fulfill({ status: HTTP_STATUS_BAD_REQUEST })
return
}
})
await get(remoteBackendPaths.LIST_VERSIONS_PATH + '*', (_route, request) => ({
versions: [
{
ami: null,
created: dateTime.toRfc3339(new Date()),
number: {
lifecycle:
// eslint-disable-next-line no-restricted-syntax
'Development' satisfies `${backend.VersionLifecycle.development}` as backend.VersionLifecycle.development,
value: '2023.2.1-dev',
},
// eslint-disable-next-line @typescript-eslint/naming-convention, no-restricted-syntax
version_type: (new URL(request.url()).searchParams.get('version_type') ??
'') as backend.VersionType,
} satisfies backend.Version,
],
}))
// === Endpoints with dummy implementations ===
await get(remoteBackendPaths.getProjectDetailsPath(GLOB_PROJECT_ID), (_route, request) => {
const projectId = backend.ProjectId(request.url().match(/[/]projects[/]([^?/]+)/)?.[1] ?? '')
const project = assetMap.get(projectId)
if (!project?.projectState) {
throw new Error('Attempting to get a project that does not exist.')
} else {
return {
organizationId: defaultOrganizationId,
projectId: projectId,
name: 'example project name',
state: project.projectState,
packageName: 'Project_root',
// eslint-disable-next-line @typescript-eslint/naming-convention
ide_version: null,
// eslint-disable-next-line @typescript-eslint/naming-convention
engine_version: {
value: '2023.2.1-nightly.2023.9.29',
lifecycle: backend.VersionLifecycle.development,
},
address: backend.Address('ws://localhost/'),
} satisfies backend.ProjectRaw
}
})
// === Endpoints returning `void` ===
await post(remoteBackendPaths.copyAssetPath(GLOB_ASSET_ID), async (route, request) => {
/** The type for the JSON request payload for this endpoint. */
interface Body {
readonly parentDirectoryId: backend.DirectoryId
}
const assetId = request.url().match(/[/]assets[/]([^?/]+)/)?.[1]
// eslint-disable-next-line no-restricted-syntax
const asset = assetId != null ? assetMap.get(assetId as backend.AssetId) : null
if (asset == null) {
if (assetId == null) {
await route.fulfill({
status: HTTP_STATUS_BAD_REQUEST,
json: { message: 'Invalid Asset ID' },
})
} else {
await route.fulfill({
status: HTTP_STATUS_NOT_FOUND,
json: { message: 'Asset does not exist' },
})
}
} else {
// The type of the body sent by this app is statically known.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const body: Body = await request.postDataJSON()
const parentId = body.parentDirectoryId
// Can be any asset ID.
const id = backend.DirectoryId(uniqueString.uniqueString())
const json: backend.CopyAssetResponse = {
asset: {
id,
parentId,
title: asset.title + ' (copy)',
},
}
const newAsset = { ...asset }
newAsset.id = id
newAsset.parentId = parentId
newAsset.title += ' (copy)'
addAsset(newAsset)
await route.fulfill({ json })
}
})
await get(remoteBackendPaths.INVITATION_PATH + '*', async route => {
await route.fulfill({
json: { invitations: [] } satisfies backend.ListInvitationsResponseBody,
})
})
await post(remoteBackendPaths.INVITE_USER_PATH + '*', async route => {
await route.fulfill()
})
await post(remoteBackendPaths.CREATE_PERMISSION_PATH + '*', async route => {
await route.fulfill()
})
await delete_(remoteBackendPaths.deleteAssetPath(GLOB_ASSET_ID), async route => {
await route.fulfill()
})
await post(remoteBackendPaths.closeProjectPath(GLOB_PROJECT_ID), async (route, request) => {
const projectId = backend.ProjectId(request.url().match(/[/]projects[/]([^?/]+)/)?.[1] ?? '')
const project = assetMap.get(projectId)
if (project?.projectState) {
object.unsafeMutable(project.projectState).type = backend.ProjectState.closed
}
await route.fulfill()
})
await post(remoteBackendPaths.openProjectPath(GLOB_PROJECT_ID), async (route, request) => {
const projectId = backend.ProjectId(request.url().match(/[/]projects[/]([^?/]+)/)?.[1] ?? '')
const project = assetMap.get(projectId)
if (project?.projectState) {
object.unsafeMutable(project.projectState).type = backend.ProjectState.opened
}
await route.fulfill()
})
await delete_(remoteBackendPaths.deleteTagPath(GLOB_TAG_ID), async route => {
await route.fulfill()
})
await post(remoteBackendPaths.POST_LOG_EVENT_PATH, async route => {
await route.fulfill()
})
// === Entity creation endpoints ===
await put(remoteBackendPaths.UPLOAD_USER_PICTURE_PATH + '*', async (route, request) => {
const content = request.postData()
if (content != null) {
currentProfilePicture = content
return null
} else {
await route.fallback()
return
}
})
await put(remoteBackendPaths.UPLOAD_ORGANIZATION_PICTURE_PATH + '*', async (route, request) => {
const content = request.postData()
if (content != null) {
currentOrganizationProfilePicture = content
return null
} else {
await route.fallback()
return
}
})
await post(remoteBackendPaths.UPLOAD_FILE_PATH + '*', (_route, request) => {
/** The type for the JSON request payload for this endpoint. */
interface SearchParams {
// eslint-disable-next-line @typescript-eslint/naming-convention
readonly file_name: string
// eslint-disable-next-line @typescript-eslint/naming-convention
readonly file_id?: backend.FileId
// eslint-disable-next-line @typescript-eslint/naming-convention
readonly parent_directory_id?: backend.DirectoryId
}
// The type of the search params sent by this app is statically known.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, no-restricted-syntax
const searchParams: SearchParams = Object.fromEntries(
new URL(request.url()).searchParams.entries()
) as never
const file = createFile(searchParams.file_name)
return { path: '', id: file.id, project: null } satisfies backend.FileInfo
})
await post(remoteBackendPaths.CREATE_SECRET_PATH + '*', async (_route, request) => {
// The type of the body sent by this app is statically known.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const body: backend.CreateSecretRequestBody = await request.postDataJSON()
const secret = createSecret(body.name)
return secret.id
})
// === Other endpoints ===
await patch(remoteBackendPaths.updateAssetPath(GLOB_ASSET_ID), (_route, request) => {
const assetId = request.url().match(/[/]assets[/]([^?]+)/)?.[1] ?? ''
// The type of the body sent by this app is statically known.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const body: backend.UpdateAssetRequestBody = request.postDataJSON()
// This could be an id for an arbitrary asset, but pretend it's a
// `DirectoryId` to make TypeScript happy.
const asset = assetMap.get(backend.DirectoryId(assetId))
if (asset != null) {
if (body.description != null) {
object.unsafeMutable(asset).description = body.description
}
}
})
await patch(remoteBackendPaths.associateTagPath(GLOB_ASSET_ID), async (_route, request) => {
const assetId = request.url().match(/[/]assets[/]([^/?]+)/)?.[1] ?? ''
/** The type for the JSON request payload for this endpoint. */
interface Body {
readonly labels: backend.LabelName[]
}
/** The type for the JSON response payload for this endpoint. */
interface Response {
readonly tags: backend.Label[]
}
// The type of the body sent by this app is statically known.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const body: Body = await request.postDataJSON()
// This could be an id for an arbitrary asset, but pretend it's a
// `DirectoryId` to make TypeScript happy.
setLabels(backend.DirectoryId(assetId), body.labels)
const json: Response = {
tags: body.labels.flatMap(value => {
const label = labelsByValue.get(value)
return label != null ? [label] : []
}),
}
return json
})
await put(remoteBackendPaths.updateDirectoryPath(GLOB_DIRECTORY_ID), async (route, request) => {
const directoryId = request.url().match(/[/]directories[/]([^?]+)/)?.[1] ?? ''
// The type of the body sent by this app is statically known.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const body: backend.UpdateDirectoryRequestBody = request.postDataJSON()
const asset = assetMap.get(backend.DirectoryId(directoryId))
if (asset == null) {
await route.abort()
} else {
object.unsafeMutable(asset).title = body.title
await route.fulfill({
json: {
id: backend.DirectoryId(directoryId),
parentId: asset.parentId,
title: body.title,
} satisfies backend.UpdatedDirectory,
})
}
})
await delete_(remoteBackendPaths.deleteAssetPath(GLOB_ASSET_ID), async (route, request) => {
const assetId = request.url().match(/[/]assets[/]([^?]+)/)?.[1] ?? ''
// This could be an id for an arbitrary asset, but pretend it's a
// `DirectoryId` to make TypeScript happy.
deleteAsset(backend.DirectoryId(assetId))
await route.fulfill({ status: HTTP_STATUS_NO_CONTENT })
})
await patch(remoteBackendPaths.UNDO_DELETE_ASSET_PATH, async (route, request) => {
/** The type for the JSON request payload for this endpoint. */
interface Body {
readonly assetId: backend.AssetId
}
// The type of the body sent by this app is statically known.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const body: Body = await request.postDataJSON()
undeleteAsset(body.assetId)
await route.fulfill({ status: HTTP_STATUS_NO_CONTENT })
})
await post(remoteBackendPaths.CREATE_USER_PATH + '*', async (route, request) => {
// The type of the body sent by this app is statically known.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const body: backend.CreateUserRequestBody = await request.postDataJSON()
const organizationId = body.organizationId ?? defaultUser.organizationId
const rootDirectoryId = backend.DirectoryId(
organizationId.replace(/^organization-/, 'directory-')
)
currentUser = {
email: body.userEmail,
name: body.userName,
organizationId,
userId: backend.UserId(`user-${uniqueString.uniqueString()}`),
isEnabled: false,
rootDirectoryId,
userGroups: null,
plan: backend.Plan.enterprise,
}
await route.fulfill({ json: currentUser })
})
await put(remoteBackendPaths.UPDATE_CURRENT_USER_PATH + '*', async (_route, request) => {
// The type of the body sent by this app is statically known.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const body: backend.UpdateUserRequestBody = await request.postDataJSON()
if (currentUser && body.username != null) {
currentUser = { ...currentUser, name: body.username }
}
})
await get(remoteBackendPaths.USERS_ME_PATH + '*', () => currentUser)
await patch(remoteBackendPaths.UPDATE_ORGANIZATION_PATH + '*', async (route, request) => {
// The type of the body sent by this app is statically known.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const body: backend.UpdateOrganizationRequestBody = await request.postDataJSON()
if (body.name === '') {
await route.fulfill({
status: HTTP_STATUS_BAD_REQUEST,
json: { message: 'Organization name must not be empty' },
})
return
} else if (currentOrganization) {
currentOrganization = { ...currentOrganization, ...body }
return currentOrganization satisfies backend.OrganizationInfo
} else {
await route.fulfill({ status: HTTP_STATUS_NOT_FOUND })
return
}
})
await get(remoteBackendPaths.GET_ORGANIZATION_PATH + '*', async route => {
await route.fulfill({
json: currentOrganization,
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
status: currentOrganization == null ? 404 : 200,
})
})
await post(remoteBackendPaths.CREATE_TAG_PATH + '*', route => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const body: backend.CreateTagRequestBody = route.request().postDataJSON()
const json: backend.Label = {
id: backend.TagId(`tag-${uniqueString.uniqueString()}`),
value: backend.LabelName(body.value),
color: body.color,
}
return json
})
await post(remoteBackendPaths.CREATE_PROJECT_PATH + '*', (_route, request) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const body: backend.CreateProjectRequestBody = request.postDataJSON()
const title = body.projectName
const id = backend.ProjectId(`project-${uniqueString.uniqueString()}`)
const parentId =
body.parentDirectoryId ?? backend.DirectoryId(`directory-${uniqueString.uniqueString()}`)
const json: backend.CreatedProject = {
name: title,
organizationId: defaultOrganizationId,
packageName: 'Project_root',
projectId: id,
state: { type: backend.ProjectState.closed, volumeId: '' },
}
addProject(title, {
description: null,
id,
labels: [],
modifiedAt: dateTime.toRfc3339(new Date()),
parentId,
permissions: [
{
user: {
organizationId: defaultOrganizationId,
userId: defaultUserId,
name: defaultUsername,
email: defaultEmail,
},
permission: permissions.PermissionAction.own,
},
],
projectState: json.state,
})
return json
})
await post(remoteBackendPaths.CREATE_DIRECTORY_PATH + '*', (_route, request) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const body: backend.CreateDirectoryRequestBody = request.postDataJSON()
const title = body.title
const id = backend.DirectoryId(`directory-${uniqueString.uniqueString()}`)
const parentId =
body.parentId ?? backend.DirectoryId(`directory-${uniqueString.uniqueString()}`)
const json: backend.CreatedDirectory = { title, id, parentId }
addDirectory(title, {
description: null,
id,
labels: [],
modifiedAt: dateTime.toRfc3339(new Date()),
parentId,
permissions: [
{
user: {
organizationId: defaultOrganizationId,
userId: defaultUserId,
name: defaultUsername,
email: defaultEmail,
},
permission: permissions.PermissionAction.own,
},
],
projectState: null,
})
return json
})
await page.route('*', async route => {
if (!isOnline) {
await route.abort('connectionfailed')
}
})
})
const api = {
defaultEmail,
defaultName: defaultUsername,
defaultOrganization,
defaultOrganizationId,
defaultOrganizationName,
defaultUser,
defaultUserId,
rootDirectoryId: defaultDirectoryId,
goOffline: () => {
isOnline = false
},
goOnline: () => {
isOnline = true
},
currentUser: () => currentUser,
setCurrentUser: (user: backend.User | null) => {
currentUser = user
},
currentPassword: () => currentPassword,
currentProfilePicture: () => currentProfilePicture,
currentOrganization: () => currentOrganization,
setCurrentOrganization: (organization: backend.OrganizationInfo | null) => {
currentOrganization = organization
},
currentOrganizationProfilePicture: () => currentOrganizationProfilePicture,
addAsset,
deleteAsset,
undeleteAsset,
createDirectory,
createProject,
createFile,
createSecret,
addDirectory,
addProject,
addFile,
addSecret,
createLabel,
addLabel,
setLabels,
addUser,
deleteUser,
addUserGroup,
deleteUserGroup,
// TODO:
// addPermission,
// deletePermission,
addUserGroupToUser,
removeUserGroupFromUser,
} as const
if (setupAPI) {
await setupAPI(api)
}
return api
}

View File

@ -0,0 +1,77 @@
/** @file Tests for the asset panel. */
import * as test from '@playwright/test'
import * as backend from '#/services/Backend'
import * as permissions from '#/utilities/permissions'
import * as actions from './actions'
// =================
// === Constants ===
// =================
/** An example description for the asset selected in the asset panel. */
const DESCRIPTION = 'foo bar'
/** An example owner username for the asset selected in the asset panel. */
const USERNAME = 'baz quux'
/** An example owner email for the asset selected in the asset panel. */
const EMAIL = 'baz.quux@email.com'
// =============
// === Tests ===
// =============
test.test('open and close asset panel', ({ page }) =>
actions
.mockAllAndLogin({ page })
.createFolder()
.driveTable.clickRow(0)
.withAssetPanel(async assetPanel => {
await actions.expectNotOnScreen(assetPanel)
})
.toggleAssetPanel()
.withAssetPanel(async assetPanel => {
await actions.expectOnScreen(assetPanel)
})
.toggleAssetPanel()
.withAssetPanel(async assetPanel => {
await actions.expectNotOnScreen(assetPanel)
})
)
test.test('asset panel contents', ({ page }) =>
actions
.mockAll({
page,
setupAPI: api => {
const { defaultOrganizationId, defaultUserId } = api
api.addProject('project', {
description: DESCRIPTION,
permissions: [
{
permission: permissions.PermissionAction.own,
user: {
organizationId: defaultOrganizationId,
// Using the default ID causes the asset to have a dynamic username.
userId: backend.UserId(defaultUserId + '2'),
name: USERNAME,
email: backend.EmailAddress(EMAIL),
},
},
],
})
},
})
.login()
.do(async thePage => {
await actions.passTermsAndConditionsDialog({ page: thePage })
})
.driveTable.clickRow(0)
.toggleAssetPanel()
.do(async () => {
await test.expect(actions.locateAssetPanelDescription(page)).toHaveText(DESCRIPTION)
// `getByText` is required so that this assertion works if there are multiple permissions.
await test.expect(actions.locateAssetPanelPermissions(page).getByText(USERNAME)).toBeVisible()
})
)

View File

@ -32,17 +32,20 @@ test.test('tags', async ({ page }) => {
})
test.test('labels', async ({ page }) => {
const { api } = await actions.mockAllAndLogin({ page })
await actions.mockAllAndLogin({
page,
setupAPI: api => {
api.addLabel('aaaa', backend.COLORS[0])
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
api.addLabel('bbbb', backend.COLORS[1]!)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
api.addLabel('cccc', backend.COLORS[2]!)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
api.addLabel('dddd', backend.COLORS[3]!)
},
})
const searchBarInput = actions.locateSearchBarInput(page)
const labels = actions.locateSearchBarLabels(page)
api.addLabel('aaaa', backend.COLORS[0])
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
api.addLabel('bbbb', backend.COLORS[1]!)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
api.addLabel('cccc', backend.COLORS[2]!)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
api.addLabel('dddd', backend.COLORS[3]!)
await actions.login({ page })
await searchBarInput.click()
for (const label of await labels.all()) {
@ -58,16 +61,21 @@ test.test('labels', async ({ page }) => {
})
test.test('suggestions', async ({ page }) => {
const { api } = await actions.mockAllAndLogin({ page })
await actions.mockAllAndLogin({
page,
setupAPI: api => {
api.addDirectory('foo')
api.addProject('bar')
api.addSecret('baz')
api.addSecret('quux')
},
})
const searchBarInput = actions.locateSearchBarInput(page)
const suggestions = actions.locateSearchBarSuggestions(page)
api.addDirectory('foo')
api.addProject('bar')
api.addSecret('baz')
api.addSecret('quux')
await actions.login({ page })
await searchBarInput.click()
for (const suggestion of await suggestions.all()) {
const name = (await suggestion.textContent()) ?? ''
test.expect(name.length).toBeGreaterThan(0)
@ -79,14 +87,18 @@ test.test('suggestions', async ({ page }) => {
})
test.test('suggestions (keyboard)', async ({ page }) => {
const { api } = await actions.mockAllAndLogin({ page })
await actions.mockAllAndLogin({
page,
setupAPI: api => {
api.addDirectory('foo')
api.addProject('bar')
api.addSecret('baz')
api.addSecret('quux')
},
})
const searchBarInput = actions.locateSearchBarInput(page)
const suggestions = actions.locateSearchBarSuggestions(page)
api.addDirectory('foo')
api.addProject('bar')
api.addSecret('baz')
api.addSecret('quux')
await actions.login({ page })
await searchBarInput.click()
for (const suggestion of await suggestions.all()) {
@ -98,14 +110,18 @@ test.test('suggestions (keyboard)', async ({ page }) => {
})
test.test('complex flows', async ({ page }) => {
const { api } = await actions.mockAllAndLogin({ page })
const searchBarInput = actions.locateSearchBarInput(page)
const firstName = 'foo'
api.addDirectory(firstName)
api.addProject('bar')
api.addSecret('baz')
api.addSecret('quux')
await actions.login({ page })
await actions.mockAllAndLogin({
page,
setupAPI: api => {
api.addDirectory(firstName)
api.addProject('bar')
api.addSecret('baz')
api.addSecret('quux')
},
})
const searchBarInput = actions.locateSearchBarInput(page)
await searchBarInput.click()
await page.press('body', 'ArrowDown')

View File

@ -0,0 +1,109 @@
/** @file Test the drive view. */
import * as test from '@playwright/test'
import * as actions from './actions'
const PASS_TIMEOUT = 5_000
test.test('extra columns should stick to right side of assets table', ({ page }) =>
actions
.mockAllAndLogin({ page })
.driveTable.toggleColumn.accessedByProjects()
.driveTable.toggleColumn.accessedData()
.withAssetsTable(async table => {
await table.evaluate(element => {
let scrollableParent: HTMLElement | SVGElement | null = element
while (
scrollableParent != null &&
scrollableParent.scrollWidth <= scrollableParent.clientWidth
) {
scrollableParent = scrollableParent.parentElement
}
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
scrollableParent?.scrollTo({ left: 999999, behavior: 'instant' })
})
})
.do(async thePage => {
const extraColumns = actions.locateExtraColumns(thePage)
const assetsTable = actions.locateAssetsTable(thePage)
await test
.expect(async () => {
const extraColumnsRight = await extraColumns.evaluate(
element => element.getBoundingClientRect().right
)
const assetsTableRight = await assetsTable.evaluate(
element => element.getBoundingClientRect().right
)
test.expect(extraColumnsRight).toEqual(assetsTableRight)
})
.toPass({ timeout: PASS_TIMEOUT })
})
)
test.test('extra columns should stick to top of scroll container', async ({ page }) => {
await actions.mockAllAndLogin({
page,
setupAPI: api => {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
for (let i = 0; i < 100; i += 1) {
api.addFile('a')
}
},
})
await actions.reload({ page })
await actions.locateAccessedByProjectsColumnToggle(page).click()
await actions.locateAccessedDataColumnToggle(page).click()
await actions.locateAssetsTable(page).evaluate(element => {
let scrollableParent: HTMLElement | SVGElement | null = element
while (
scrollableParent != null &&
scrollableParent.scrollWidth <= scrollableParent.clientWidth
) {
scrollableParent = scrollableParent.parentElement
}
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
scrollableParent?.scrollTo({ top: 999999, behavior: 'instant' })
})
const extraColumns = actions.locateExtraColumns(page)
const assetsTable = actions.locateAssetsTable(page)
await test
.expect(async () => {
const extraColumnsTop = await extraColumns.evaluate(
element => element.getBoundingClientRect().top
)
const assetsTableTop = await assetsTable.evaluate(element => {
let scrollableParent: HTMLElement | SVGElement | null = element
while (
scrollableParent != null &&
scrollableParent.scrollWidth <= scrollableParent.clientWidth
) {
scrollableParent = scrollableParent.parentElement
}
return scrollableParent?.getBoundingClientRect().top
})
test.expect(extraColumnsTop).toEqual(assetsTableTop)
})
.toPass({ timeout: PASS_TIMEOUT })
})
test.test('can drop onto root directory dropzone', ({ page }) =>
actions
.mockAllAndLogin({ page })
.createFolder()
.uploadFile('b', 'testing')
.driveTable.doubleClickRow(0)
.driveTable.withRows(async rows => {
const parentLeft = await actions.getAssetRowLeftPx(rows.nth(0))
const childLeft = await actions.getAssetRowLeftPx(rows.nth(1))
test.expect(childLeft, 'Child is indented further than parent').toBeGreaterThan(parentLeft)
})
.driveTable.dragRow(1, actions.locateRootDirectoryDropzone(page))
.driveTable.withRows(async rows => {
const firstLeft = await actions.getAssetRowLeftPx(rows.nth(0))
// The second row is the indented child of the directory
// (the "this folder is empty" row).
const secondLeft = await actions.getAssetRowLeftPx(rows.nth(2))
test.expect(firstLeft, 'Siblings have same indentation').toEqual(secondLeft)
})
)

View File

@ -0,0 +1,188 @@
/** @file Test copying, moving, cutting and pasting. */
import * as test from '@playwright/test'
import * as actions from './actions'
// =============
// === Tests ===
// =============
test.test('copy', ({ page }) =>
actions
.mockAllAndLogin({ page })
// Assets: [0: Folder 1]
.createFolder()
// Assets: [0: Folder 2, 1: Folder 1]
.createFolder()
.driveTable.rightClickRow(0)
// Assets: [0: Folder 2 <copied>, 1: Folder 1]
.contextMenu.copy()
.driveTable.rightClickRow(1)
// Assets: [0: Folder 2, 1: Folder 1, 2: Folder 2 (copy) <child { depth=1 }>]
.contextMenu.paste()
.driveTable.withRows(async rows => {
await test.expect(rows).toHaveCount(3)
await test.expect(rows.nth(2)).toBeVisible()
await test.expect(rows.nth(2)).toHaveText(/^New Folder 2 [(]copy[)]/)
const parentLeft = await actions.getAssetRowLeftPx(rows.nth(1))
const childLeft = await actions.getAssetRowLeftPx(rows.nth(2))
test.expect(childLeft, 'child is indented further than parent').toBeGreaterThan(parentLeft)
})
)
test.test('copy (keyboard)', ({ page }) =>
actions
.mockAllAndLogin({ page })
// Assets: [0: Folder 1]
.createFolder()
// Assets: [0: Folder 2, 1: Folder 1]
.createFolder()
.driveTable.clickRow(0)
// Assets: [0: Folder 2 <copied>, 1: Folder 1]
.press('Mod+C')
.driveTable.clickRow(1)
// Assets: [0: Folder 2, 1: Folder 1, 2: Folder 2 (copy) <child { depth=1 }>]
.press('Mod+V')
.driveTable.withRows(async rows => {
await test.expect(rows).toHaveCount(3)
await test.expect(rows.nth(2)).toBeVisible()
await test.expect(rows.nth(2)).toHaveText(/^New Folder 2 [(]copy[)]/)
const parentLeft = await actions.getAssetRowLeftPx(rows.nth(1))
const childLeft = await actions.getAssetRowLeftPx(rows.nth(2))
test.expect(childLeft, 'child is indented further than parent').toBeGreaterThan(parentLeft)
})
)
test.test('move', ({ page }) =>
actions
.mockAllAndLogin({ page })
// Assets: [0: Folder 1]
.createFolder()
// Assets: [0: Folder 2, 1: Folder 1]
.createFolder()
.driveTable.rightClickRow(0)
// Assets: [0: Folder 2 <cut>, 1: Folder 1]
.contextMenu.cut()
.driveTable.rightClickRow(1)
// Assets: [0: Folder 1, 1: Folder 2 <child { depth=1 }>]
.contextMenu.paste()
.driveTable.withRows(async rows => {
await test.expect(rows).toHaveCount(2)
await test.expect(rows.nth(1)).toBeVisible()
await test.expect(rows.nth(1)).toHaveText(/^New Folder 2/)
const parentLeft = await actions.getAssetRowLeftPx(rows.nth(0))
const childLeft = await actions.getAssetRowLeftPx(rows.nth(1))
test.expect(childLeft, 'child is indented further than parent').toBeGreaterThan(parentLeft)
})
)
test.test('move (drag)', ({ page }) =>
actions
.mockAllAndLogin({ page })
// Assets: [0: Folder 1]
.createFolder()
// Assets: [0: Folder 2, 1: Folder 1]
.createFolder()
// Assets: [0: Folder 1, 1: Folder 2 <child { depth=1 }>]
.driveTable.dragRowToRow(0, 1)
.driveTable.withRows(async rows => {
await test.expect(rows).toHaveCount(2)
await test.expect(rows.nth(1)).toBeVisible()
await test.expect(rows.nth(1)).toHaveText(/^New Folder 2/)
const parentLeft = await actions.getAssetRowLeftPx(rows.nth(0))
const childLeft = await actions.getAssetRowLeftPx(rows.nth(1))
test.expect(childLeft, 'child is indented further than parent').toBeGreaterThan(parentLeft)
})
)
test.test('move to trash', ({ page }) =>
actions
.mockAllAndLogin({ page })
// Assets: [0: Folder 1]
.createFolder()
// Assets: [0: Folder 2, 1: Folder 1]
.createFolder()
// NOTE: For some reason, `react-aria-components` causes drag-n-drop to break if `Mod` is still
// held.
.withModPressed(modActions => modActions.driveTable.clickRow(0).driveTable.clickRow(1))
.driveTable.dragRow(0, actions.locateTrashCategory(page))
.driveTable.expectPlaceholderRow()
.goToCategory.trash()
.driveTable.withRows(async rows => {
await test.expect(rows).toHaveText([/^New Folder 1/, /^New Folder 2/])
})
)
test.test('move (keyboard)', ({ page }) =>
actions
.mockAllAndLogin({ page })
// Assets: [0: Folder 1]
.createFolder()
// Assets: [0: Folder 2, 1: Folder 1]
.createFolder()
.driveTable.clickRow(0)
// Assets: [0: Folder 2 <cut>, 1: Folder 1]
.press('Mod+X')
.driveTable.clickRow(1)
// Assets: [0: Folder 1, 1: Folder 2 <child { depth=1 }>]
.press('Mod+V')
.driveTable.withRows(async rows => {
await test.expect(rows).toHaveCount(2)
await test.expect(rows.nth(1)).toBeVisible()
await test.expect(rows.nth(1)).toHaveText(/^New Folder 2/)
const parentLeft = await actions.getAssetRowLeftPx(rows.nth(0))
const childLeft = await actions.getAssetRowLeftPx(rows.nth(1))
test.expect(childLeft, 'child is indented further than parent').toBeGreaterThan(parentLeft)
})
)
test.test('cut (keyboard)', async ({ page }) =>
actions
.mockAllAndLogin({ page })
.createFolder()
.driveTable.clickRow(0)
.press('Mod+X')
.driveTable.withRows(async rows => {
// This action is not a builtin `expect` action, so it needs to be manually retried.
await test
.expect(async () => {
test
.expect(await rows.nth(0).evaluate(el => Number(getComputedStyle(el).opacity)))
.toBeLessThan(1)
})
.toPass()
})
)
test.test('duplicate', ({ page }) =>
actions
.mockAllAndLogin({ page })
// Assets: [0: New Project 1]
.newEmptyProject()
.goToPage.drive()
.driveTable.rightClickRow(0)
.contextMenu.duplicate()
.driveTable.withRows(async rows => {
// Assets: [0: New Project 1 (copy), 1: New Project 1]
await test.expect(rows).toHaveCount(2)
await test.expect(actions.locateContextMenus(page)).not.toBeVisible()
await test.expect(rows.nth(0)).toBeVisible()
await test.expect(rows.nth(0)).toHaveText(/^New Project 1 [(]copy[)]/)
})
)
test.test('duplicate (keyboard)', ({ page }) =>
actions
.mockAllAndLogin({ page })
// Assets: [0: New Project 1]
.newEmptyProject()
.goToPage.drive()
.driveTable.clickRow(0)
.press('Mod+D')
.driveTable.withRows(async rows => {
// Assets: [0: New Project 1 (copy), 1: New Project 1]
await test.expect(rows).toHaveCount(2)
await test.expect(rows.nth(0)).toBeVisible()
await test.expect(rows.nth(0)).toHaveText(/^New Project 1 [(]copy[)]/)
})
)

View File

@ -0,0 +1,63 @@
/** @file Test copying, moving, cutting and pasting. */
import * as test from '@playwright/test'
import * as actions from './actions'
// =================
// === Constants ===
// =================
/** The name of the uploaded file. */
const FILE_NAME = 'foo.txt'
/** The contents of the uploaded file. */
const FILE_CONTENTS = 'hello world'
/** The name of the created secret. */
const SECRET_NAME = 'a secret name'
/** The value of the created secret. */
const SECRET_VALUE = 'a secret value'
// =============
// === Tests ===
// =============
test.test('create folder', ({ page }) =>
actions
.mockAllAndLogin({ page })
.createFolder()
.driveTable.withRows(async rows => {
await test.expect(rows).toHaveCount(1)
await test.expect(rows.nth(0)).toBeVisible()
await test.expect(rows.nth(0)).toHaveText(/^New Folder 1/)
})
)
test.test('create project', ({ page }) =>
actions
.mockAllAndLogin({ page })
.newEmptyProject()
.do(thePage => test.expect(actions.locateEditor(thePage)).toBeAttached())
.goToPage.drive()
.driveTable.withRows(rows => test.expect(rows).toHaveCount(1))
)
test.test('upload file', ({ page }) =>
actions
.mockAllAndLogin({ page })
.uploadFile(FILE_NAME, FILE_CONTENTS)
.driveTable.withRows(async rows => {
await test.expect(rows).toHaveCount(1)
await test.expect(rows.nth(0)).toBeVisible()
await test.expect(rows.nth(0)).toHaveText(new RegExp('^' + FILE_NAME))
})
)
test.test('create secret', ({ page }) =>
actions
.mockAllAndLogin({ page })
.createSecret(SECRET_NAME, SECRET_VALUE)
.driveTable.withRows(async rows => {
await test.expect(rows).toHaveCount(1)
await test.expect(rows.nth(0)).toBeVisible()
await test.expect(rows.nth(0)).toHaveText(new RegExp('^' + SECRET_NAME))
})
)

View File

@ -0,0 +1,15 @@
/** @file Test the user settings tab. */
import * as test from '@playwright/test'
import * as actions from './actions'
const DATA_LINK_NAME = 'a data link'
test.test('data link editor', ({ page }) =>
actions
.mockAllAndLogin({ page })
.openDataLinkModal()
.withNameInput(async input => {
await input.fill(DATA_LINK_NAME)
})
)

View File

@ -0,0 +1,50 @@
/** @file Test copying, moving, cutting and pasting. */
import * as test from '@playwright/test'
import * as actions from './actions'
test.test('delete and restore', ({ page }) =>
actions
.mockAllAndLogin({ page })
.createFolder()
.driveTable.withRows(async rows => {
await test.expect(rows).toHaveCount(1)
})
.driveTable.rightClickRow(0)
.contextMenu.moveToTrash()
.driveTable.expectPlaceholderRow()
.goToCategory.trash()
.driveTable.withRows(async rows => {
await test.expect(rows).toHaveCount(1)
})
.driveTable.rightClickRow(0)
.contextMenu.restoreFromTrash()
.driveTable.expectTrashPlaceholderRow()
.goToCategory.cloud()
.driveTable.withRows(async rows => {
await test.expect(rows).toHaveCount(1)
})
)
test.test('delete and restore (keyboard)', ({ page }) =>
actions
.mockAllAndLogin({ page })
.createFolder()
.driveTable.withRows(async rows => {
await test.expect(rows).toHaveCount(1)
})
.driveTable.clickRow(0)
.press('Delete')
.driveTable.expectPlaceholderRow()
.goToCategory.trash()
.driveTable.withRows(async rows => {
await test.expect(rows).toHaveCount(1)
})
.driveTable.clickRow(0)
.press('Mod+R')
.driveTable.expectTrashPlaceholderRow()
.goToCategory.cloud()
.driveTable.withRows(async rows => {
await test.expect(rows).toHaveCount(1)
})
)

View File

@ -0,0 +1,44 @@
/** @file Test the drive view. */
import * as test from '@playwright/test'
import * as actions from './actions'
test.test('drive view', ({ page }) =>
actions
.mockAllAndLogin({ page })
.withDriveView(async view => {
await test.expect(view).toBeVisible()
})
.driveTable.expectPlaceholderRow()
.newEmptyProject()
.do(async () => {
await test.expect(actions.locateEditor(page)).toBeAttached()
})
.goToPage.drive()
.driveTable.withRows(async rows => {
await test.expect(rows).toHaveCount(1)
})
.do(async () => {
await test.expect(actions.locateAssetsTable(page)).toBeVisible()
})
.newEmptyProject()
.do(async () => {
await test.expect(actions.locateEditor(page)).toBeAttached()
})
.goToPage.drive()
.driveTable.withRows(async rows => {
await test.expect(rows).toHaveCount(2)
})
// The last opened project needs to be stopped, to remove the toast notification notifying the
// user that project creation may take a while. Previously opened projects are stopped when the
// new project is created.
.driveTable.withRows(async rows => {
await actions.locateStopProjectButton(rows.nth(0)).click()
})
// Project context menu
.driveTable.rightClickRow(0)
.contextMenu.moveToTrash()
.driveTable.withRows(async rows => {
await test.expect(rows).toHaveCount(1)
})
)

View File

@ -0,0 +1,87 @@
/** @file Test copying, moving, cutting and pasting. */
import * as test from '@playwright/test'
import * as actions from './actions'
test.test.beforeEach(({ page }) => actions.mockAllAndLogin({ page }))
test.test('edit name', async ({ page }) => {
const assetRows = actions.locateAssetRows(page)
const mod = await actions.modModifier(page)
const row = assetRows.nth(0)
const newName = 'foo bar baz'
await actions.locateNewFolderIcon(page).click()
await actions.locateAssetRowName(row).click({ modifiers: [mod] })
await actions.locateAssetRowName(row).fill(newName)
await actions.locateEditingTick(row).click()
await test.expect(row).toHaveText(new RegExp('^' + newName))
})
test.test('edit name (keyboard)', async ({ page }) => {
const assetRows = actions.locateAssetRows(page)
const row = assetRows.nth(0)
const newName = 'foo bar baz quux'
await actions.locateNewFolderIcon(page).click()
await actions.locateAssetRowName(row).click()
await actions.press(page, 'Mod+R')
await actions.locateAssetRowName(row).fill(newName)
await actions.locateAssetRowName(row).press('Enter')
await test.expect(row).toHaveText(new RegExp('^' + newName))
})
test.test('cancel editing name', async ({ page }) => {
const assetRows = actions.locateAssetRows(page)
const mod = await actions.modModifier(page)
const row = assetRows.nth(0)
const newName = 'foo bar baz'
await actions.locateNewFolderIcon(page).click()
const oldName = (await actions.locateAssetRowName(row).textContent()) ?? ''
await actions.locateAssetRowName(row).click({ modifiers: [mod] })
await actions.locateAssetRowName(row).fill(newName)
await actions.locateEditingCross(row).click()
await test.expect(row).toHaveText(new RegExp('^' + oldName))
})
test.test('cancel editing name (keyboard)', async ({ page }) => {
const assetRows = actions.locateAssetRows(page)
const row = assetRows.nth(0)
const newName = 'foo bar baz quux'
await actions.locateNewFolderIcon(page).click()
const oldName = (await actions.locateAssetRowName(row).textContent()) ?? ''
await actions.locateAssetRowName(row).click()
await actions.press(page, 'Mod+R')
await actions.locateAssetRowName(row).fill(newName)
await actions.locateAssetRowName(row).press('Escape')
await test.expect(row).toHaveText(new RegExp('^' + oldName))
})
test.test('change to blank name', async ({ page }) => {
const assetRows = actions.locateAssetRows(page)
const mod = await actions.modModifier(page)
const row = assetRows.nth(0)
await actions.locateNewFolderIcon(page).click()
const oldName = (await actions.locateAssetRowName(row).textContent()) ?? ''
await actions.locateAssetRowName(row).click({ modifiers: [mod] })
await actions.locateAssetRowName(row).fill('')
await test.expect(actions.locateEditingTick(row)).not.toBeVisible()
await actions.locateEditingCross(row).click()
await test.expect(row).toHaveText(new RegExp('^' + oldName))
})
test.test('change to blank name (keyboard)', async ({ page }) => {
const assetRows = actions.locateAssetRows(page)
const row = assetRows.nth(0)
await actions.locateNewFolderIcon(page).click()
const oldName = (await actions.locateAssetRowName(row).textContent()) ?? ''
await actions.locateAssetRowName(row).click()
await actions.press(page, 'Mod+R')
await actions.locateAssetRowName(row).fill('')
await actions.locateAssetRowName(row).press('Enter')
await test.expect(row).toHaveText(new RegExp('^' + oldName))
})

View File

@ -0,0 +1,69 @@
/** @file Test dragging of labels. */
import * as test from '@playwright/test'
import * as backend from '#/services/Backend'
import * as actions from './actions'
test.test('drag labels onto single row', async ({ page }) => {
const label = 'aaaa'
await actions.mockAllAndLogin({
page,
setupAPI: api => {
api.addLabel(label, backend.COLORS[0])
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
api.addLabel('bbbb', backend.COLORS[1]!)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
api.addLabel('cccc', backend.COLORS[2]!)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
api.addLabel('dddd', backend.COLORS[3]!)
api.addDirectory('foo')
api.addSecret('bar')
api.addFile('baz')
api.addSecret('quux')
},
})
const assetRows = actions.locateAssetRows(page)
const labelEl = actions.locateLabelsPanelLabels(page, label)
await actions.relog({ page })
await test.expect(labelEl).toBeVisible()
await labelEl.dragTo(assetRows.nth(1))
await test.expect(actions.locateAssetLabels(assetRows.nth(0)).getByText(label)).not.toBeVisible()
await test.expect(actions.locateAssetLabels(assetRows.nth(1)).getByText(label)).toBeVisible()
await test.expect(actions.locateAssetLabels(assetRows.nth(2)).getByText(label)).not.toBeVisible()
await test.expect(actions.locateAssetLabels(assetRows.nth(3)).getByText(label)).not.toBeVisible()
})
test.test('drag labels onto multiple rows', async ({ page }) => {
const label = 'aaaa'
await actions.mockAllAndLogin({
page,
setupAPI: api => {
api.addLabel(label, backend.COLORS[0])
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
api.addLabel('bbbb', backend.COLORS[1]!)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
api.addLabel('cccc', backend.COLORS[2]!)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
api.addLabel('dddd', backend.COLORS[3]!)
api.addDirectory('foo')
api.addSecret('bar')
api.addFile('baz')
api.addSecret('quux')
},
})
const assetRows = actions.locateAssetRows(page)
const labelEl = actions.locateLabelsPanelLabels(page, label)
await page.keyboard.down(await actions.modModifier(page))
await actions.clickAssetRow(assetRows.nth(0))
await actions.clickAssetRow(assetRows.nth(2))
await test.expect(labelEl).toBeVisible()
await labelEl.dragTo(assetRows.nth(2))
await page.keyboard.up(await actions.modModifier(page))
await test.expect(actions.locateAssetLabels(assetRows.nth(0)).getByText(label)).toBeVisible()
await test.expect(actions.locateAssetLabels(assetRows.nth(1)).getByText(label)).not.toBeVisible()
await test.expect(actions.locateAssetLabels(assetRows.nth(2)).getByText(label)).toBeVisible()
await test.expect(actions.locateAssetLabels(assetRows.nth(3)).getByText(label)).not.toBeVisible()
})

View File

@ -3,7 +3,7 @@ import * as test from '@playwright/test'
import * as actions from './actions'
test.test.beforeEach(actions.mockAllAndLogin)
test.test.beforeEach(({ page }) => actions.mockAllAndLogin({ page }))
test.test('labels', async ({ page }) => {
// Empty labels panel

View File

@ -0,0 +1,25 @@
/** @file Test the login flow. */
import * as test from '@playwright/test'
import * as actions from './actions'
// =============
// === Tests ===
// =============
test.test('login and logout', ({ page }) =>
actions
.mockAll({ page })
.login()
.do(async thePage => {
await actions.passTermsAndConditionsDialog({ page: thePage })
await test.expect(actions.locateDriveView(thePage)).toBeVisible()
await test.expect(actions.locateLoginButton(thePage)).not.toBeVisible()
})
.openUserMenu()
.userMenu.logout()
.do(async thePage => {
await test.expect(actions.locateDriveView(thePage)).not.toBeVisible()
await test.expect(actions.locateLoginButton(thePage)).toBeVisible()
})
)

View File

@ -3,7 +3,7 @@ import * as test from '@playwright/test'
import * as actions from './actions'
test.test.beforeEach(actions.mockAll)
test.test.beforeEach(({ page }) => actions.mockAll({ page }))
// =============
// === Tests ===

View File

@ -0,0 +1,34 @@
/** @file Test the user settings tab. */
import * as test from '@playwright/test'
import * as actions from './actions'
test.test('members settings', async ({ page }) => {
const api = await actions.mockAllAndLoginAndExposeAPI({ page })
const localActions = actions.settings.members
// Setup
api.setCurrentOrganization(api.defaultOrganization)
await localActions.go(page)
await test
.expect(localActions.locateMembersRows(page).locator('> :nth-child(1) > :nth-child(2)'))
.toHaveText([api.currentUser()?.name ?? ''])
const otherUserName = 'second.user_'
const otherUser = api.addUser(otherUserName)
// await actions.closeModal(page)
await actions.relog({ page })
await localActions.go(page)
await test
.expect(localActions.locateMembersRows(page).locator('> :nth-child(1) > :nth-child(2)'))
.toHaveText([api.currentUser()?.name ?? '', otherUserName])
api.deleteUser(otherUser.userId)
// await actions.closeModal(page)
await actions.relog({ page })
await localActions.go(page)
await test
.expect(localActions.locateMembersRows(page).locator('> :nth-child(1) > :nth-child(2)'))
.toHaveText([api.currentUser()?.name ?? ''])
})

View File

@ -0,0 +1,94 @@
/** @file Test the organization settings tab. */
import * as test from '@playwright/test'
import * as actions from './actions'
test.test('organization settings', async ({ page }) => {
const api = await actions.mockAllAndLoginAndExposeAPI({ page })
const localActions = actions.settings.organization
// Setup
api.setCurrentOrganization(api.defaultOrganization)
await test.test.step('Initial state', () => {
test.expect(api.currentOrganization()?.name).toBe(api.defaultOrganizationName)
test.expect(api.currentOrganization()?.email).toBe(null)
test.expect(api.currentOrganization()?.picture).toBe(null)
test.expect(api.currentOrganization()?.website).toBe(null)
test.expect(api.currentOrganization()?.address).toBe(null)
})
await test.expect(page.getByText('Logging in to Enso...')).not.toBeVisible()
await localActions.go(page)
const nameInput = localActions.locateNameInput(page)
const newName = 'another organization-name'
await test.test.step('Set name', async () => {
await nameInput.fill(newName)
await nameInput.press('Enter')
test.expect(api.currentOrganization()?.name).toBe(newName)
test.expect(api.currentUser()?.name).not.toBe(newName)
})
await test.test.step('Unset name (should fail)', async () => {
await nameInput.fill('')
await nameInput.press('Enter')
test.expect(api.currentOrganization()?.name).toBe(newName)
await test.expect(nameInput).toHaveValue(newName)
})
const invalidEmail = 'invalid@email'
const emailInput = localActions.locateEmailInput(page)
await test.test.step('Set invalid email', async () => {
await emailInput.fill(invalidEmail)
await emailInput.press('Enter')
test.expect(api.currentOrganization()?.email).toBe(null)
})
const newEmail = 'organization@email.com'
await test.test.step('Set email', async () => {
await emailInput.fill(newEmail)
await emailInput.press('Enter')
test.expect(api.currentOrganization()?.email).toBe(newEmail)
await test.expect(emailInput).toHaveValue(newEmail)
})
const websiteInput = localActions.locateWebsiteInput(page)
const newWebsite = 'organization.org'
// NOTE: It's not yet possible to unset the website or the location.
await test.test.step('Set website', async () => {
await websiteInput.fill(newWebsite)
await websiteInput.press('Enter')
test.expect(api.currentOrganization()?.website).toBe(newWebsite)
await test.expect(websiteInput).toHaveValue(newWebsite)
})
const locationInput = localActions.locateLocationInput(page)
const newLocation = 'Somewhere, CA'
await test.test.step('Set location', async () => {
await locationInput.fill(newLocation)
await locationInput.press('Enter')
test.expect(api.currentOrganization()?.address).toBe(newLocation)
await test.expect(locationInput).toHaveValue(newLocation)
})
})
test.test('upload organization profile picture', async ({ page }) => {
const api = await actions.mockAllAndLoginAndExposeAPI({ page })
const localActions = actions.settings.organizationProfilePicture
await localActions.go(page)
const fileChooserPromise = page.waitForEvent('filechooser')
await localActions.locateInput(page).click()
const fileChooser = await fileChooserPromise
const name = 'bar.jpeg'
const content = 'organization profile picture'
await fileChooser.setFiles([{ name, buffer: Buffer.from(content), mimeType: 'image/jpeg' }])
await test
.expect(() => {
test.expect(api.currentOrganizationProfilePicture()).toEqual(content)
})
.toPass()
})

View File

@ -0,0 +1,21 @@
/** @file Test the login flow. */
import * as test from '@playwright/test'
import * as actions from './actions'
test.test('page switcher', ({ page }) =>
actions
.mockAllAndLogin({ page })
// Create a new project so that the editor page can be switched to.
.newEmptyProject()
.goToPage.drive()
.do(async thePage => {
await test.expect(actions.locateDriveView(thePage)).toBeVisible()
await test.expect(actions.locateEditor(thePage)).not.toBeVisible()
})
.goToPage.editor()
.do(async thePage => {
await test.expect(actions.locateDriveView(thePage)).not.toBeVisible()
await test.expect(actions.locateEditor(thePage)).toBeVisible()
})
)

View File

@ -0,0 +1,123 @@
/** @file Test the login flow. */
import * as test from '@playwright/test'
import * as actions from './actions'
import type * as api from './api'
// =================
// === Constants ===
// =================
const EMAIL = 'example.email+1234@testing.org'
const NAME = 'a custom user name'
const ORGANIZATION_ID = 'some testing organization id'
// =============
// === Tests ===
// =============
// Note: This does not check that the organization ID is sent in the correct format for the backend.
// It only checks that the organization ID is sent in certain places.
test.test('sign up with organization id', async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('domcontentloaded')
await page.goto(
'/registration?' + new URLSearchParams([['organization_id', ORGANIZATION_ID]]).toString()
)
const api = await actions.mockApi({ page })
api.setCurrentUser(null)
// Sign up
await actions.locateEmailInput(page).fill(actions.VALID_EMAIL)
await actions.locatePasswordInput(page).fill(actions.VALID_PASSWORD)
await actions.locateConfirmPasswordInput(page).fill(actions.VALID_PASSWORD)
await actions.locateRegisterButton(page).click()
// Log in
await actions.locateEmailInput(page).fill(actions.VALID_EMAIL)
await actions.locatePasswordInput(page).fill(actions.VALID_PASSWORD)
await actions.locateLoginButton(page).click()
await actions.passTermsAndConditionsDialog({ page })
// Set username
await actions.locateUsernameInput(page).fill('arbitrary username')
await actions.locateSetUsernameButton(page).click()
test
.expect(api.currentUser()?.organizationId, 'new user has correct organization id')
.toBe(ORGANIZATION_ID)
})
test.test('sign up without organization id', async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('domcontentloaded')
await page.goto('/registration')
const api = await actions.mockApi({ page })
api.setCurrentUser(null)
// Sign up
await actions.locateEmailInput(page).fill(actions.VALID_EMAIL)
await actions.locatePasswordInput(page).fill(actions.VALID_PASSWORD)
await actions.locateConfirmPasswordInput(page).fill(actions.VALID_PASSWORD)
await actions.locateRegisterButton(page).click()
// Log in
await actions.locateEmailInput(page).fill(actions.VALID_EMAIL)
await actions.locatePasswordInput(page).fill(actions.VALID_PASSWORD)
await actions.locateLoginButton(page).click()
await actions.passTermsAndConditionsDialog({ page })
// Set username
await actions.locateUsernameInput(page).fill('arbitrary username')
await actions.locateSetUsernameButton(page).click()
test
.expect(api.currentUser()?.organizationId, 'new user has correct organization id')
.toBe(api.defaultOrganizationId)
})
test.test('sign up flow', ({ page }) => {
let api!: api.MockApi
return actions
.mockAll({
page,
setupAPI: theApi => {
api = theApi
theApi.setCurrentUser(null)
// These values should be different, otherwise the email and name may come from the defaults.
test.expect(EMAIL).not.toStrictEqual(theApi.defaultEmail)
test.expect(NAME).not.toStrictEqual(theApi.defaultName)
},
})
.loginAsNewUser(EMAIL, actions.VALID_PASSWORD)
.do(async thePage => {
await actions.passTermsAndConditionsDialog({ page: thePage })
})
.setUsername(NAME)
.do(async thePage => {
await test.expect(actions.locateUpgradeButton(thePage)).toBeVisible()
await test.expect(actions.locateDriveView(thePage)).not.toBeVisible()
})
.do(() => {
// Logged in, and account enabled
const currentUser = api.currentUser()
test.expect(currentUser).toBeDefined()
if (currentUser != null) {
// This is required because `UserOrOrganization` is `readonly`.
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, no-restricted-syntax, no-extra-semi
;(currentUser as { isEnabled: boolean }).isEnabled = true
}
})
.openUserMenu()
.userMenu.logout()
.login(EMAIL, actions.VALID_PASSWORD)
.do(async () => {
await test.expect(actions.locateNotEnabledStub(page)).not.toBeVisible()
await test.expect(actions.locateDriveView(page)).toBeVisible()
})
.do(() => {
test.expect(api.currentUser()?.email, 'new user has correct email').toBe(EMAIL)
test.expect(api.currentUser()?.name, 'new user has correct name').toBe(NAME)
})
})

View File

@ -7,48 +7,59 @@ import * as actions from './actions'
/* eslint-disable @typescript-eslint/no-magic-numbers */
// =================
// === Constants ===
// =================
const START_DATE_EPOCH_MS = 1.7e12
/** The number of milliseconds in a minute. */
const MIN_MS = 60_000
// =============
// === Tests ===
// =============
test.test('sort', async ({ page }) => {
const { api } = await actions.mockAll({ page })
await actions.mockAll({
page,
setupAPI: api => {
const date1 = dateTime.toRfc3339(new Date(START_DATE_EPOCH_MS))
const date2 = dateTime.toRfc3339(new Date(START_DATE_EPOCH_MS + 1 * MIN_MS))
const date3 = dateTime.toRfc3339(new Date(START_DATE_EPOCH_MS + 2 * MIN_MS))
const date4 = dateTime.toRfc3339(new Date(START_DATE_EPOCH_MS + 3 * MIN_MS))
const date5 = dateTime.toRfc3339(new Date(START_DATE_EPOCH_MS + 4 * MIN_MS))
const date6 = dateTime.toRfc3339(new Date(START_DATE_EPOCH_MS + 5 * MIN_MS))
const date7 = dateTime.toRfc3339(new Date(START_DATE_EPOCH_MS + 6 * MIN_MS))
const date8 = dateTime.toRfc3339(new Date(START_DATE_EPOCH_MS + 7 * MIN_MS))
api.addDirectory('a directory', { modifiedAt: date4 })
api.addDirectory('G directory', { modifiedAt: date6 })
api.addProject('C project', { modifiedAt: date7 })
api.addSecret('H secret', { modifiedAt: date2 })
api.addProject('b project', { modifiedAt: date1 })
api.addFile('d file', { modifiedAt: date8 })
api.addSecret('f secret', { modifiedAt: date3 })
api.addFile('e file', { modifiedAt: date5 })
// By date:
// b project
// h secret
// f secret
// a directory
// e file
// g directory
// c project
// d file
},
})
const assetRows = actions.locateAssetRows(page)
const nameHeading = actions.locateNameColumnHeading(page)
const modifiedHeading = actions.locateModifiedColumnHeading(page)
const date1 = dateTime.toRfc3339(new Date(START_DATE_EPOCH_MS))
const date2 = dateTime.toRfc3339(new Date(START_DATE_EPOCH_MS + 1 * MIN_MS))
const date3 = dateTime.toRfc3339(new Date(START_DATE_EPOCH_MS + 2 * MIN_MS))
const date4 = dateTime.toRfc3339(new Date(START_DATE_EPOCH_MS + 3 * MIN_MS))
const date5 = dateTime.toRfc3339(new Date(START_DATE_EPOCH_MS + 4 * MIN_MS))
const date6 = dateTime.toRfc3339(new Date(START_DATE_EPOCH_MS + 5 * MIN_MS))
const date7 = dateTime.toRfc3339(new Date(START_DATE_EPOCH_MS + 6 * MIN_MS))
const date8 = dateTime.toRfc3339(new Date(START_DATE_EPOCH_MS + 7 * MIN_MS))
api.addDirectory('a directory', { modifiedAt: date4 })
api.addDirectory('G directory', { modifiedAt: date6 })
api.addProject('C project', { modifiedAt: date7 })
api.addSecret('H secret', { modifiedAt: date2 })
api.addProject('b project', { modifiedAt: date1 })
api.addFile('d file', { modifiedAt: date8 })
api.addSecret('f secret', { modifiedAt: date3 })
api.addFile('e file', { modifiedAt: date5 })
// By date:
// b project
// h secret
// f secret
// a directory
// e file
// g directory
// c project
// d file
await page.goto('/')
await actions.login({ page })
// By default, assets should be grouped by type.
// Assets in each group are ordered by insertion order.
await actions.expectTransparent(actions.locateSortAscendingIcon(nameHeading))
await actions.expectOpacity0(actions.locateSortAscendingIcon(nameHeading))
await test.expect(actions.locateSortDescendingIcon(nameHeading)).not.toBeVisible()
await actions.expectTransparent(actions.locateSortAscendingIcon(modifiedHeading))
await actions.expectOpacity0(actions.locateSortAscendingIcon(modifiedHeading))
await test.expect(actions.locateSortDescendingIcon(modifiedHeading)).not.toBeVisible()
await test.expect(assetRows.nth(0)).toHaveText(/^a directory/)
await test.expect(assetRows.nth(1)).toHaveText(/^G directory/)
@ -61,7 +72,7 @@ test.test('sort', async ({ page }) => {
// Sort by name ascending.
await nameHeading.click()
await actions.expectNotTransparent(actions.locateSortAscendingIcon(nameHeading))
await actions.expectNotOpacity0(actions.locateSortAscendingIcon(nameHeading))
await test.expect(assetRows.nth(0)).toHaveText(/^a directory/)
await test.expect(assetRows.nth(1)).toHaveText(/^b project/)
await test.expect(assetRows.nth(2)).toHaveText(/^C project/)
@ -73,7 +84,7 @@ test.test('sort', async ({ page }) => {
// Sort by name descending.
await nameHeading.click()
await actions.expectNotTransparent(actions.locateSortDescendingIcon(nameHeading))
await actions.expectNotOpacity0(actions.locateSortDescendingIcon(nameHeading))
await test.expect(assetRows.nth(0)).toHaveText(/^H secret/)
await test.expect(assetRows.nth(1)).toHaveText(/^G directory/)
await test.expect(assetRows.nth(2)).toHaveText(/^f secret/)
@ -86,7 +97,7 @@ test.test('sort', async ({ page }) => {
// Sorting should be unset.
await nameHeading.click()
await page.mouse.move(0, 0)
await actions.expectTransparent(actions.locateSortAscendingIcon(nameHeading))
await actions.expectOpacity0(actions.locateSortAscendingIcon(nameHeading))
await test.expect(actions.locateSortDescendingIcon(nameHeading)).not.toBeVisible()
await test.expect(assetRows.nth(0)).toHaveText(/^a directory/)
await test.expect(assetRows.nth(1)).toHaveText(/^G directory/)
@ -99,7 +110,7 @@ test.test('sort', async ({ page }) => {
// Sort by date ascending.
await modifiedHeading.click()
await actions.expectNotTransparent(actions.locateSortAscendingIcon(modifiedHeading))
await actions.expectNotOpacity0(actions.locateSortAscendingIcon(modifiedHeading))
await test.expect(assetRows.nth(0)).toHaveText(/^b project/)
await test.expect(assetRows.nth(1)).toHaveText(/^H secret/)
await test.expect(assetRows.nth(2)).toHaveText(/^f secret/)
@ -111,7 +122,7 @@ test.test('sort', async ({ page }) => {
// Sort by date descending.
await modifiedHeading.click()
await actions.expectNotTransparent(actions.locateSortDescendingIcon(modifiedHeading))
await actions.expectNotOpacity0(actions.locateSortDescendingIcon(modifiedHeading))
await test.expect(assetRows.nth(0)).toHaveText(/^d file/)
await test.expect(assetRows.nth(1)).toHaveText(/^C project/)
await test.expect(assetRows.nth(2)).toHaveText(/^G directory/)
@ -124,7 +135,7 @@ test.test('sort', async ({ page }) => {
// Sorting should be unset.
await modifiedHeading.click()
await page.mouse.move(0, 0)
await actions.expectTransparent(actions.locateSortAscendingIcon(modifiedHeading))
await actions.expectOpacity0(actions.locateSortAscendingIcon(modifiedHeading))
await test.expect(actions.locateSortDescendingIcon(modifiedHeading)).not.toBeVisible()
await test.expect(assetRows.nth(0)).toHaveText(/^a directory/)
await test.expect(assetRows.nth(1)).toHaveText(/^G directory/)

View File

@ -0,0 +1,15 @@
/** @file Test the "change password" modal. */
import * as test from '@playwright/test'
import * as actions from './actions'
test.test('create project from template', ({ page }) =>
actions
.mockAllAndLogin({ page })
.openStartModal()
.createProjectFromTemplate(0)
.do(async thePage => {
await test.expect(actions.locateEditor(thePage)).toBeAttached()
await test.expect(actions.locateSamples(page).first()).not.toBeVisible()
})
)

View File

@ -0,0 +1,23 @@
/** @file Test the user menu. */
import * as test from '@playwright/test'
import * as actions from './actions'
test.test('user menu', ({ page }) =>
actions
.mockAllAndLogin({ page })
.openUserMenu()
.do(async thePage => {
await test.expect(actions.locateUserMenu(thePage)).toBeVisible()
})
)
test.test('download app', ({ page }) =>
actions
.mockAllAndLogin({ page })
.openUserMenu()
.userMenu.downloadApp(async download => {
await download.cancel()
test.expect(download.url()).toMatch(/^https:[/][/]objects.githubusercontent.com/)
})
)

View File

@ -0,0 +1,97 @@
/** @file Test the user settings tab. */
import * as test from '@playwright/test'
import * as actions from './actions'
test.test('user settings', async ({ page }) => {
const api = await actions.mockAllAndLoginAndExposeAPI({ page })
const localActions = actions.settings.userAccount
test.expect(api.currentUser()?.name).toBe(api.defaultName)
await localActions.go(page)
const nameInput = localActions.locateNameInput(page)
const newName = 'another user-name'
await nameInput.fill(newName)
await nameInput.press('Enter')
test.expect(api.currentUser()?.name).toBe(newName)
test.expect(api.currentOrganization()?.name).not.toBe(newName)
})
test.test('change password form', async ({ page }) => {
const api = await actions.mockAllAndLoginAndExposeAPI({ page })
const localActions = actions.settings.changePassword
await localActions.go(page)
test.expect(api.currentPassword()).toBe(actions.VALID_PASSWORD)
await localActions.locateCurrentPasswordInput(page).fill(actions.VALID_PASSWORD)
await localActions.locateNewPasswordInput(page).fill(actions.INVALID_PASSWORD)
await test
.expect(localActions.locateChangeButton(page), 'incomplete form should be rejected')
.toBeDisabled()
await test.test.step('Invalid new password', async () => {
await localActions.locateCurrentPasswordInput(page).fill(actions.VALID_PASSWORD)
await localActions.locateNewPasswordInput(page).fill(actions.INVALID_PASSWORD)
await localActions.locateConfirmNewPasswordInput(page).fill(actions.INVALID_PASSWORD)
test
.expect(
await localActions
.locateNewPasswordInput(page)
.evaluate((element: HTMLInputElement) => element.validity.valid),
'invalid new password should be rejected'
)
.toBe(false)
await test
.expect(localActions.locateChangeButton(page), 'invalid new password should be rejected')
.toBeDisabled()
})
await test.test.step('Invalid new password confirmation', async () => {
await localActions.locateCurrentPasswordInput(page).fill(actions.VALID_PASSWORD)
await localActions.locateNewPasswordInput(page).fill(actions.VALID_PASSWORD)
await localActions.locateConfirmNewPasswordInput(page).fill(actions.VALID_PASSWORD + 'a')
test
.expect(
await localActions
.locateConfirmNewPasswordInput(page)
.evaluate((element: HTMLInputElement) => element.validity.valid),
'invalid new password confirmation should be rejected'
)
.toBe(false)
await test
.expect(
localActions.locateChangeButton(page),
'invalid new password confirmation should be rejected'
)
.toBeDisabled()
})
await test.test.step('Successful password change', async () => {
const newPassword = '1234!' + actions.VALID_PASSWORD
await localActions.locateNewPasswordInput(page).fill(newPassword)
await localActions.locateConfirmNewPasswordInput(page).fill(newPassword)
await localActions.locateChangeButton(page).click()
await test.expect(localActions.locateCurrentPasswordInput(page)).toHaveText('')
await test.expect(localActions.locateNewPasswordInput(page)).toHaveText('')
await test.expect(localActions.locateConfirmNewPasswordInput(page)).toHaveText('')
test.expect(api.currentPassword()).toBe(newPassword)
})
})
test.test('upload profile picture', async ({ page }) => {
const api = await actions.mockAllAndLoginAndExposeAPI({ page })
const localActions = actions.settings.profilePicture
await localActions.go(page)
const fileChooserPromise = page.waitForEvent('filechooser')
await localActions.locateInput(page).click()
const fileChooser = await fileChooserPromise
const name = 'foo.png'
const content = 'a profile picture'
await fileChooser.setFiles([{ name, mimeType: 'image/png', buffer: Buffer.from(content) }])
await test
.expect(() => {
test.expect(api.currentProfilePicture()).toEqual(content)
})
.toPass()
})

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -16,14 +16,15 @@
<meta
http-equiv="Content-Security-Policy"
content="
default-src 'self';
frame-src 'self' data: https://js.stripe.com;
script-src 'self' 'unsafe-eval' data: https://*;
style-src 'self' 'unsafe-inline' data: https://*;
connect-src 'self' data: ws://localhost:* ws://127.0.0.1:* http://localhost:* https://* wss://*;
worker-src 'self' blob:;
img-src 'self' blob: data: https://*;
font-src 'self' data: https://*"
default-src 'self';
frame-src 'self' data: https://js.stripe.com;
script-src 'self' 'unsafe-eval' data: https://*;
script-src-elem 'self' 'unsafe-inline' https://*;
style-src 'self' 'unsafe-inline' data: https://*;
connect-src 'self' data: ws://localhost:* ws://127.0.0.1:* http://localhost:* https://* wss://*;
worker-src 'self' blob:;
img-src 'self' blob: data: https://*;
font-src 'self' data: https://*"
/>
<meta
name="viewport"
@ -37,16 +38,11 @@
<script type="module" src="./src/entrypoint.ts" defer></script>
</head>
<body>
<div id="app"></div>
<div id="enso-dashboard" class="enso-dashboard"></div>
<div id="enso-chat" class="enso-chat"></div>
<div id="enso-portal-root" class="enso-portal-root"></div>
<noscript>
This page requires JavaScript to run. Please enable it in your browser.
</noscript>
<!-- Google tag (gtag.js) -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-CLTBJ37MDM"
></script>
</body>
</html>

View File

@ -19,68 +19,65 @@
"build": "vite build",
"dev": "vite",
"dev:e2e": "vite -c vite.test.config.ts",
"dev:e2e:ci": "vite -c vite.test.config.ts build && vite preview --port 8080 --strictPort",
"test": "npm run test:unit && npm run test:e2e",
"test:unit": "vitest run",
"test:unit:debug": "vitest",
"test:e2e": "cross-env NODE_ENV=production playwright test",
"test:e2e:debug": "cross-env NODE_ENV=production playwright test --ui"
},
"//": [
"@fortawesome/fontawesome-svg-core is required as a peer dependency for @fortawesome/react-fontawesome"
],
"dependencies": {
"@aws-amplify/auth": "5.6.5",
"@aws-amplify/core": "5.8.5",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-brands-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@hookform/resolvers": "^3.4.0",
"@monaco-editor/react": "4.6.0",
"@sentry/react": "^7.74.0",
"@tanstack/react-query": "^5.27.5",
"@stripe/react-stripe-js": "^2.7.1",
"@stripe/stripe-js": "^3.5.0",
"@tanstack/react-query": "5.45.1",
"@tanstack/vue-query": ">= 5.45.0 < 5.46.0",
"ajv": "^8.12.0",
"clsx": "^1.1.1",
"enso-assets": "workspace:*",
"clsx": "^2.1.1",
"enso-common": "workspace:*",
"is-network-error": "^1.0.1",
"monaco-editor": "0.47.0",
"react": "^18.2.0",
"react-aria": "^3.32.1",
"react-aria-components": "^1.1.1",
"react-dom": "^18.2.0",
"react-router": "^6.8.1",
"react-router-dom": "^6.8.1",
"react-stately": "^3.30.1",
"monaco-editor": "0.48.0",
"react": "^18.3.1",
"react-aria": "^3.33.0",
"react-aria-components": "^1.2.0",
"react-dom": "^18.3.1",
"react-error-boundary": "4.0.13",
"react-hook-form": "^7.51.4",
"react-router": "^6.23.1",
"react-router-dom": "^6.23.1",
"react-stately": "^3.31.0",
"react-toastify": "^9.1.3",
"tailwind-merge": "^2.2.1",
"tailwind-merge": "^2.3.0",
"tailwind-variants": "0.2.1",
"tiny-invariant": "^1.3.3",
"ts-results": "^3.3.0",
"validator": "^13.11.0"
"validator": "^13.12.0",
"zod": "^3.23.8",
"zustand": "^4.5.4"
},
"devDependencies": {
"@babel/plugin-syntax-import-assertions": "^7.23.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@fast-check/vitest": "^0.0.8",
"@ianvs/prettier-plugin-sort-imports": "^4.1.1",
"@modyfi/vite-plugin-yaml": "^1.0.4",
"@playwright/experimental-ct-react": "^1.40.0",
"@playwright/test": "^1.40.0",
"@react-types/shared": "^3.22.1",
"@tanstack/react-query-devtools": "5.45.1",
"@types/eslint__js": "^8.42.3",
"@types/node": "^20.11.21",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/validator": "^13.11.7",
"@typescript-eslint/eslint-plugin": "^6.7.2",
"@typescript-eslint/parser": "^6.7.2",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
"@vitejs/plugin-react": "^4.2.1",
"chalk": "^5.3.0",
"cross-env": "^7.0.3",
"enso-chat": "git://github.com/enso-org/enso-bot",
"esbuild": "^0.19.3",
"esbuild-plugin-inline-image": "^0.0.9",
"esbuild-plugin-time": "^1.0.0",
"esbuild-plugin-yaml": "^0.0.1",
"eslint": "^8.49.0",
"eslint-plugin-jsdoc": "^46.8.1",
"eslint-plugin-react": "^7.32.1",
"fast-check": "^3.15.0",
"playwright": "^1.38.0",
@ -88,15 +85,14 @@
"prettier-plugin-tailwindcss": "^0.5.11",
"react-toastify": "^9.1.3",
"tailwindcss": "^3.4.1",
"tailwindcss-animate": "1.0.7",
"tailwindcss-react-aria-components": "^1.1.1",
"ts-plugin-namespace-auto-import": "workspace:*",
"typescript": "~5.2.2",
"vite": "^5.2.10",
"typescript": "^5.5.3",
"vite": "^5.3.3",
"vitest": "^1.3.1"
},
"optionalDependencies": {
"@esbuild/darwin-x64": "^0.17.15",
"@esbuild/linux-x64": "^0.17.15",
"@esbuild/windows-x64": "^0.17.15"
"overrides": {
"@aws-amplify/auth": "../_IGNORED_",
"react-native-url-polyfill": "../_IGNORED_"
}
}

View File

@ -6,19 +6,26 @@
* default fonts. */
import * as test from '@playwright/test'
import * as appConfig from 'enso-common/src/appConfig'
appConfig.loadTestEnvironmentVariables()
/* eslint-disable @typescript-eslint/no-magic-numbers, @typescript-eslint/strict-boolean-expressions */
const DEBUG = process.env.PWDEBUG === '1'
const TIMEOUT_MS = DEBUG ? 100_000_000 : 30_000
export default test.defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: true,
workers: 1,
workers: process.env.PROD ? 8 : 1,
repeatEach: process.env.CI ? 3 : 1,
expect: {
toHaveScreenshot: { threshold: 0 },
timeout: 30_000,
timeout: TIMEOUT_MS,
},
timeout: 30_000,
timeout: TIMEOUT_MS,
reporter: 'html',
use: {
baseURL: 'http://localhost:8080',
@ -26,8 +33,12 @@ export default test.defineConfig({
launchOptions: {
ignoreDefaultArgs: ['--headless'],
args: [
// Much closer to headful Chromium than classic headless.
'--headless=new',
...(DEBUG
? []
: [
// Much closer to headful Chromium than classic headless.
'--headless=new',
]),
// Required for `backdrop-filter: blur` to work.
'--use-angle=swiftshader',
// FIXME: `--disable-gpu` disables `backdrop-filter: blur`, which is not handled by
@ -46,7 +57,7 @@ export default test.defineConfig({
},
},
webServer: {
command: 'npm run dev:e2e',
command: process.env.CI || process.env.PROD ? 'npm run dev:e2e:ci' : 'npm run dev:e2e',
port: 8080,
reuseExistingServer: false,
},

View File

@ -42,41 +42,55 @@ import * as toastify from 'react-toastify'
import * as detect from 'enso-common/src/detect'
import * as appUtils from '#/appUtils'
import * as reactQueryClientModule from '#/reactQueryClient'
import * as inputBindingsModule from '#/configurations/inputBindings'
import * as backendHooks from '#/hooks/backendHooks'
import AuthProvider, * as authProvider from '#/providers/AuthProvider'
import BackendProvider from '#/providers/BackendProvider'
import * as httpClientProvider from '#/providers/HttpClientProvider'
import InputBindingsProvider from '#/providers/InputBindingsProvider'
import LocalStorageProvider, * as localStorageProvider from '#/providers/LocalStorageProvider'
import LoggerProvider from '#/providers/LoggerProvider'
import type * as loggerProvider from '#/providers/LoggerProvider'
import ModalProvider from '#/providers/ModalProvider'
import ModalProvider, * as modalProvider from '#/providers/ModalProvider'
import * as navigator2DProvider from '#/providers/Navigator2DProvider'
import SessionProvider from '#/providers/SessionProvider'
import * as textProvider from '#/providers/TextProvider'
import ConfirmRegistration from '#/pages/authentication/ConfirmRegistration'
import EnterOfflineMode from '#/pages/authentication/EnterOfflineMode'
import ErrorScreen from '#/pages/authentication/ErrorScreen'
import ForgotPassword from '#/pages/authentication/ForgotPassword'
import LoadingScreen from '#/pages/authentication/LoadingScreen'
import Login from '#/pages/authentication/Login'
import Registration from '#/pages/authentication/Registration'
import ResetPassword from '#/pages/authentication/ResetPassword'
import RestoreAccount from '#/pages/authentication/RestoreAccount'
import SetUsername from '#/pages/authentication/SetUsername'
import Dashboard from '#/pages/dashboard/Dashboard'
import Subscribe from '#/pages/subscribe/Subscribe'
import * as subscribe from '#/pages/subscribe/Subscribe'
import * as subscribeSuccess from '#/pages/subscribe/SubscribeSuccess'
import type * as editor from '#/layouts/Editor'
import * as openAppWatcher from '#/layouts/OpenAppWatcher'
import * as devtools from '#/components/Devtools'
import * as errorBoundary from '#/components/ErrorBoundary'
import * as offlineNotificationManager from '#/components/OfflineNotificationManager'
import * as rootComponent from '#/components/Root'
import * as suspense from '#/components/Suspense'
import AboutModal from '#/modals/AboutModal'
import * as setOrganizationNameModal from '#/modals/SetOrganizationNameModal'
import * as termsOfServiceModal from '#/modals/TermsOfServiceModal'
import type Backend from '#/services/Backend'
import LocalBackend from '#/services/LocalBackend'
import * as projectManager from '#/services/ProjectManager'
import ProjectManager from '#/services/ProjectManager'
import RemoteBackend from '#/services/RemoteBackend'
import * as appBaseUrl from '#/utilities/appBaseUrl'
import * as eventModule from '#/utilities/event'
import type HttpClient from '#/utilities/HttpClient'
import LocalStorage from '#/utilities/LocalStorage'
import * as object from '#/utilities/object'
@ -139,7 +153,11 @@ export interface AppProps {
readonly initialProjectName: string | null
readonly onAuthenticated: (accessToken: string | null) => void
readonly projectManagerUrl: string | null
readonly appRunner: AppRunner
readonly ydocUrl: string | null
readonly appRunner: editor.GraphEditorRunner | null
readonly portalRoot: Element
readonly httpClient: HttpClient
readonly queryClient: reactQuery.QueryClient
}
/** Component called by the parent module, returning the root React component for this
@ -148,53 +166,81 @@ export interface AppProps {
* This component handles all the initialization and rendering of the app, and manages the app's
* routes. It also initializes an `AuthProvider` that will be used by the rest of the app. */
export default function App(props: AppProps) {
const { supportsLocalBackend } = props
// This is a React component even though it does not contain JSX.
// eslint-disable-next-line no-restricted-syntax
const Router = detect.isOnElectron() ? router.HashRouter : router.BrowserRouter
const queryClient = React.useMemo(() => reactQueryClientModule.createReactQueryClient(), [])
const [rootDirectoryPath, setRootDirectoryPath] = React.useState<projectManager.Path | null>(null)
const [error, setError] = React.useState<unknown>(null)
const isLoading = supportsLocalBackend && rootDirectoryPath == null
const {
data: { projectManagerRootDirectory, projectManagerInstance },
} = reactQuery.useSuspenseQuery<{
projectManagerInstance: ProjectManager | null
projectManagerRootDirectory: projectManager.Path | null
}>({
queryKey: [
'root-directory',
{
projectManagerUrl: props.projectManagerUrl,
supportsLocalBackend: props.supportsLocalBackend,
},
] as const,
meta: { persist: false },
networkMode: 'always',
staleTime: Infinity,
gcTime: Infinity,
refetchOnMount: false,
refetchInterval: false,
refetchOnReconnect: false,
refetchIntervalInBackground: false,
behavior: {
onFetch: ({ state }) => {
const instance = state.data?.projectManagerInstance ?? null
React.useEffect(() => {
if (supportsLocalBackend) {
void (async () => {
try {
const response = await fetch(`${appBaseUrl.APP_BASE_URL}/api/root-directory`)
const text = await response.text()
setRootDirectoryPath(projectManager.Path(text))
} catch (innerError) {
setError(innerError)
if (instance != null) {
void instance.dispose()
}
})()
}
}, [supportsLocalBackend])
},
},
queryFn: async () => {
if (props.supportsLocalBackend && props.projectManagerUrl != null) {
const response = await fetch(`${appBaseUrl.APP_BASE_URL}/api/root-directory`)
const text = await response.text()
const rootDirectory = projectManager.Path(text)
return {
projectManagerInstance: new ProjectManager(props.projectManagerUrl, rootDirectory),
projectManagerRootDirectory: rootDirectory,
}
} else {
return {
projectManagerInstance: null,
projectManagerRootDirectory: null,
}
}
},
})
// Both `BackendProvider` and `InputBindingsProvider` depend on `LocalStorageProvider`.
// Note that the `Router` must be the parent of the `AuthProvider`, because the `AuthProvider`
// will redirect the user between the login/register pages and the dashboard.
return error != null ? (
<ErrorScreen error={error} />
) : isLoading ? (
<LoadingScreen />
) : (
<reactQuery.QueryClientProvider client={queryClient}>
return (
<>
<toastify.ToastContainer
position="top-center"
theme="light"
closeOnClick={false}
draggable={false}
toastClassName="text-sm leading-cozy bg-selected-frame rounded-default backdrop-blur-default"
toastClassName="text-sm leading-cozy bg-selected-frame rounded-lg backdrop-blur-default"
transition={toastify.Zoom}
limit={3}
/>
<Router basename={getMainPageUrl().pathname}>
<router.BrowserRouter basename={getMainPageUrl().pathname}>
<LocalStorageProvider>
<AppRouter {...props} projectManagerRootDirectory={rootDirectoryPath} />
<ModalProvider>
<AppRouter
{...props}
projectManagerInstance={projectManagerInstance}
projectManagerRootDirectory={projectManagerRootDirectory}
/>
</ModalProvider>
</LocalStorageProvider>
</Router>
</reactQuery.QueryClientProvider>
</router.BrowserRouter>
</>
)
}
@ -205,6 +251,7 @@ export default function App(props: AppProps) {
/** Props for an {@link AppRouter}. */
export interface AppRouterProps extends AppProps {
readonly projectManagerRootDirectory: projectManager.Path | null
readonly projectManagerInstance: ProjectManager | null
}
/** Router definition for the app.
@ -213,22 +260,37 @@ export interface AppRouterProps extends AppProps {
* because the {@link AppRouter} relies on React hooks, which can't be used in the same React
* component as the component that defines the provider. */
function AppRouter(props: AppRouterProps) {
const { logger, supportsLocalBackend, isAuthenticationDisabled, shouldShowDashboard } = props
const { onAuthenticated, projectManagerUrl, projectManagerRootDirectory } = props
const { logger, isAuthenticationDisabled, shouldShowDashboard, httpClient } = props
const { onAuthenticated, projectManagerInstance } = props
const { portalRoot } = props
// `navigateHooks.useNavigate` cannot be used here as it relies on `AuthProvider`, which has not
// yet been initialized at this point.
// eslint-disable-next-line no-restricted-properties
const navigate = router.useNavigate()
const { getText } = textProvider.useText()
const { localStorage } = localStorageProvider.useLocalStorage()
const { setModal } = modalProvider.useSetModal()
const navigator2D = navigator2DProvider.useNavigator2D()
const localBackend = React.useMemo(
() => (projectManagerInstance != null ? new LocalBackend(projectManagerInstance) : null),
[projectManagerInstance]
)
const remoteBackend = React.useMemo(
() => new RemoteBackend(httpClient, logger, getText),
[httpClient, logger, getText]
)
backendHooks.useObserveBackend(remoteBackend)
backendHooks.useObserveBackend(localBackend)
if (detect.IS_DEV_MODE) {
// @ts-expect-error This is used exclusively for debugging.
window.navigate = navigate
}
const [inputBindingsRaw] = React.useState(() => inputBindingsModule.createBindings())
const [root] = React.useState<React.RefObject<HTMLElement>>(() => ({
current: document.getElementById('enso-dashboard'),
}))
React.useEffect(() => {
const savedInputBindings = localStorage.get('inputBindings')
@ -246,7 +308,7 @@ function AppRouter(props: AppRouterProps) {
}
}
}
}, [/* should never change */ localStorage, /* should never change */ inputBindingsRaw])
}, [localStorage, inputBindingsRaw])
const inputBindings = React.useMemo(() => {
const updateLocalStorage = () => {
@ -294,25 +356,27 @@ function AppRouter(props: AppRouterProps) {
return inputBindingsRaw.unregister.bind(inputBindingsRaw)
},
}
}, [/* should never change */ localStorage, /* should never change */ inputBindingsRaw])
}, [localStorage, inputBindingsRaw])
const mainPageUrl = getMainPageUrl()
const authService = React.useMemo(() => {
const authConfig = { navigate, ...props }
return authServiceModule.initAuthService(authConfig)
}, [props, /* should never change */ navigate])
}, [props, navigate])
const userSession = authService?.cognito.userSession.bind(authService.cognito) ?? null
const refreshUserSession =
authService?.cognito.refreshUserSession.bind(authService.cognito) ?? null
const registerAuthEventListener = authService?.registerAuthEventListener ?? null
const initialBackend: Backend =
isAuthenticationDisabled && projectManagerUrl != null && projectManagerRootDirectory != null
? new LocalBackend(projectManagerUrl, projectManagerRootDirectory)
: // This is SAFE, because the backend is always set by the authentication flow.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
null!
React.useEffect(() => {
if ('menuApi' in window) {
window.menuApi.setShowAboutModalHandler(() => {
setModal(<AboutModal />)
})
}
}, [setModal])
React.useEffect(() => {
const onKeyDown = navigator2D.onKeyDown.bind(navigator2D)
@ -331,7 +395,8 @@ function AppRouter(props: AppRouterProps) {
if (
isClick &&
!eventModule.isElementTextInput(event.target) &&
!eventModule.isElementPartOfMonaco(event.target)
!eventModule.isElementPartOfMonaco(event.target) &&
!eventModule.isElementTextInput(document.activeElement)
) {
const selection = document.getSelection()
const app = document.getElementById('app')
@ -342,15 +407,15 @@ function AppRouter(props: AppRouterProps) {
app.contains(selection.anchorNode) &&
selection.focusNode != null &&
app.contains(selection.focusNode)
if (selection != null && !appContainsSelection) {
selection.removeAllRanges()
if (!appContainsSelection) {
selection?.removeAllRanges()
}
}
}
const onSelectStart = () => {
isClick = false
}
document.addEventListener('mousedown', onMouseDown)
document.addEventListener('mouseup', onMouseUp)
document.addEventListener('selectstart', onSelectStart)
@ -363,86 +428,135 @@ function AppRouter(props: AppRouterProps) {
const routes = (
<router.Routes>
<React.Fragment>
{/* Login & registration pages are visible to unauthenticated users. */}
<router.Route element={<authProvider.GuestLayout />}>
<router.Route path={appUtils.REGISTRATION_PATH} element={<Registration />} />
<router.Route
path={appUtils.LOGIN_PATH}
element={<Login supportsLocalBackend={supportsLocalBackend} />}
/>
</router.Route>
{/* Login & registration pages are visible to unauthenticated users. */}
<router.Route element={<authProvider.GuestLayout />}>
<router.Route path={appUtils.REGISTRATION_PATH} element={<Registration />} />
<router.Route path={appUtils.LOGIN_PATH} element={<Login />} />
</router.Route>
{/* Protected pages are visible to authenticated users. */}
<router.Route element={<authProvider.NotDeletedUserLayout />}>
<router.Route element={<authProvider.ProtectedLayout />}>
<router.Route
element={
detect.IS_DEV_MODE ? (
<devtools.EnsoDevtools>
<router.Outlet />
</devtools.EnsoDevtools>
) : null
}
>
<router.Route element={<termsOfServiceModal.TermsOfServiceModal />}>
<router.Route element={<setOrganizationNameModal.SetOrganizationNameModal />}>
<router.Route element={<openAppWatcher.OpenAppWatcher />}>
<router.Route
path={appUtils.DASHBOARD_PATH}
element={shouldShowDashboard && <Dashboard {...props} />}
/>
<router.Route
path={appUtils.SUBSCRIBE_PATH}
element={
<errorBoundary.ErrorBoundary>
<suspense.Suspense>
<subscribe.Subscribe />
</suspense.Suspense>
</errorBoundary.ErrorBoundary>
}
/>
</router.Route>
</router.Route>
</router.Route>
{/* Protected pages are visible to authenticated users. */}
<router.Route element={<authProvider.NotDeletedUserLayout />}>
<router.Route element={<authProvider.ProtectedLayout />}>
<router.Route
path={appUtils.DASHBOARD_PATH}
element={shouldShowDashboard && <Dashboard {...props} />}
path={appUtils.SUBSCRIBE_SUCCESS_PATH}
element={
<errorBoundary.ErrorBoundary>
<suspense.Suspense>
<subscribeSuccess.SubscribeSuccess />
</suspense.Suspense>
</errorBoundary.ErrorBoundary>
}
/>
<router.Route path={appUtils.SUBSCRIBE_PATH} element={<Subscribe />} />
</router.Route>
</router.Route>
</router.Route>
<router.Route element={<termsOfServiceModal.TermsOfServiceModal />}>
{/* Semi-protected pages are visible to users currently registering. */}
<router.Route element={<authProvider.NotDeletedUserLayout />}>
<router.Route element={<authProvider.SemiProtectedLayout />}>
<router.Route path={appUtils.SET_USERNAME_PATH} element={<SetUsername />} />
</router.Route>
</router.Route>
</router.Route>
{/* Other pages are visible to unauthenticated and authenticated users. */}
<router.Route path={appUtils.CONFIRM_REGISTRATION_PATH} element={<ConfirmRegistration />} />
<router.Route path={appUtils.FORGOT_PASSWORD_PATH} element={<ForgotPassword />} />
<router.Route path={appUtils.RESET_PASSWORD_PATH} element={<ResetPassword />} />
<router.Route path={appUtils.ENTER_OFFLINE_MODE_PATH} element={<EnterOfflineMode />} />
{/* Other pages are visible to unauthenticated and authenticated users. */}
<router.Route path={appUtils.CONFIRM_REGISTRATION_PATH} element={<ConfirmRegistration />} />
<router.Route path={appUtils.FORGOT_PASSWORD_PATH} element={<ForgotPassword />} />
<router.Route path={appUtils.RESET_PASSWORD_PATH} element={<ResetPassword />} />
{/* Soft-deleted user pages are visible to users who have been soft-deleted. */}
<router.Route element={<authProvider.ProtectedLayout />}>
<router.Route element={<authProvider.SoftDeletedUserLayout />}>
<router.Route path={appUtils.RESTORE_USER_PATH} element={<RestoreAccount />} />
</router.Route>
{/* Soft-deleted user pages are visible to users who have been soft-deleted. */}
<router.Route element={<authProvider.ProtectedLayout />}>
<router.Route element={<authProvider.SoftDeletedUserLayout />}>
<router.Route path={appUtils.RESTORE_USER_PATH} element={<RestoreAccount />} />
</router.Route>
</React.Fragment>
</router.Route>
{/* 404 page */}
<router.Route path="*" element={<router.Navigate to="/" replace />} />
</router.Routes>
)
let result = routes
result = <errorBoundary.ErrorBoundary>{result}</errorBoundary.ErrorBoundary>
result = <InputBindingsProvider inputBindings={inputBindings}>{result}</InputBindingsProvider>
result = <ModalProvider>{result}</ModalProvider>
result = (
<AuthProvider
shouldStartInOfflineMode={isAuthenticationDisabled}
supportsLocalBackend={supportsLocalBackend}
authService={authService}
onAuthenticated={onAuthenticated}
projectManagerUrl={projectManagerUrl}
projectManagerRootDirectory={projectManagerRootDirectory}
>
{result}
</AuthProvider>
)
result = <BackendProvider initialBackend={initialBackend}>{result}</BackendProvider>
result = (
<BackendProvider remoteBackend={remoteBackend} localBackend={localBackend}>
{result}
</BackendProvider>
)
result = (
<SessionProvider
saveAccessToken={authService?.cognito.saveAccessToken.bind(authService.cognito) ?? null}
mainPageUrl={mainPageUrl}
userSession={userSession}
registerAuthEventListener={registerAuthEventListener}
refreshUserSession={
refreshUserSession
? async () => {
await refreshUserSession()
}
: null
}
refreshUserSession={refreshUserSession}
>
{result}
</SessionProvider>
)
result = <LoggerProvider logger={logger}>{result}</LoggerProvider>
result = (
<rootComponent.Root rootRef={root} navigate={navigate}>
<rootComponent.Root navigate={navigate} portalRoot={portalRoot}>
{result}
</rootComponent.Root>
)
result = (
<offlineNotificationManager.OfflineNotificationManager>
{result}
</offlineNotificationManager.OfflineNotificationManager>
)
result = (
<httpClientProvider.HttpClientProvider httpClient={httpClient}>
{result}
</httpClientProvider.HttpClientProvider>
)
result = <LoggerProvider logger={logger}>{result}</LoggerProvider>
return result
}

View File

@ -0,0 +1,8 @@
/** @file Placeholder component for GUI used during e2e tests. */
import type * as editor from '#/layouts/Editor'
/** Placeholder component for GUI used during e2e tests. */
export function TestAppRunner(props: editor.GraphEditorProps) {
// eslint-disable-next-line no-restricted-syntax
return props.hidden ? <></> : <div data-testid="gui-editor-root">Vue app loads here.</div>
}

View File

@ -4,9 +4,7 @@
// === Constants ===
// =================
// =============
// === Paths ===
// =============
/** Path to the root of the app (i.e., the Cloud dashboard). */
export const DASHBOARD_PATH = '/'
@ -16,10 +14,8 @@ export const LOGIN_PATH = '/login'
export const REGISTRATION_PATH = '/registration'
/** Path to the confirm registration page. */
export const CONFIRM_REGISTRATION_PATH = '/confirmation'
/**
* Path to the page in which a user can restore their account after it has been
* marked for deletion.
*/
/** Path to the page in which a user can restore their account after it has been
* marked for deletion. */
export const RESTORE_USER_PATH = '/restore-user'
/** Path to the forgot password page. */
export const FORGOT_PASSWORD_PATH = '/forgot-password'
@ -28,18 +24,30 @@ export const RESET_PASSWORD_PATH = '/password-reset'
/** Path to the set username page. */
export const SET_USERNAME_PATH = '/set-username'
/** Path to the offline mode entrypoint. */
export const ENTER_OFFLINE_MODE_PATH = '/offline'
/** Path to page in which the currently active payment plan can be managed. */
export const SUBSCRIBE_PATH = '/subscribe'
export const SUBSCRIBE_SUCCESS_PATH = '/subscribe/success'
/** A {@link RegExp} matching all paths. */
export const ALL_PATHS_REGEX = new RegExp(
`(?:${DASHBOARD_PATH}|${LOGIN_PATH}|${REGISTRATION_PATH}|${CONFIRM_REGISTRATION_PATH}|` +
`${FORGOT_PASSWORD_PATH}|${RESET_PASSWORD_PATH}|${SET_USERNAME_PATH}|${RESTORE_USER_PATH}|` +
`${ENTER_OFFLINE_MODE_PATH}|${SUBSCRIBE_PATH})$`
`${SUBSCRIBE_PATH}|${SUBSCRIBE_SUCCESS_PATH})$`
)
// ===========
// === URL ===
// ===========
// === Constants related to URLs ===
export const SEARCH_PARAMS_PREFIX = 'cloud-ide_'
/**
* Build a Subscription URL for a given plan.
*/
export function getUpgradeURL(plan: string): string {
return SUBSCRIBE_PATH + '?plan=' + plan
}
/**
* Build a Subscription URL for contacting sales.
*/
export function getContactSalesURL(): string {
return 'mailto:contact@enso.org?subject=Upgrading%20to%20Organization%20Plan'
}

View File

Before

Width:  |  Height:  |  Size: 503 B

After

Width:  |  Height:  |  Size: 503 B

View File

Before

Width:  |  Height:  |  Size: 408 B

After

Width:  |  Height:  |  Size: 408 B

View File

Before

Width:  |  Height:  |  Size: 889 B

After

Width:  |  Height:  |  Size: 889 B

View File

Before

Width:  |  Height:  |  Size: 913 B

After

Width:  |  Height:  |  Size: 913 B

View File

Before

Width:  |  Height:  |  Size: 703 B

After

Width:  |  Height:  |  Size: 703 B

View File

Before

Width:  |  Height:  |  Size: 548 B

After

Width:  |  Height:  |  Size: 548 B

View File

Before

Width:  |  Height:  |  Size: 261 B

After

Width:  |  Height:  |  Size: 261 B

View File

Before

Width:  |  Height:  |  Size: 303 B

After

Width:  |  Height:  |  Size: 303 B

View File

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 269 B

View File

Before

Width:  |  Height:  |  Size: 454 B

After

Width:  |  Height:  |  Size: 454 B

View File

Before

Width:  |  Height:  |  Size: 327 B

After

Width:  |  Height:  |  Size: 327 B

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 998 B

After

Width:  |  Height:  |  Size: 998 B

View File

Before

Width:  |  Height:  |  Size: 102 B

After

Width:  |  Height:  |  Size: 102 B

View File

Before

Width:  |  Height:  |  Size: 102 B

After

Width:  |  Height:  |  Size: 102 B

View File

Before

Width:  |  Height:  |  Size: 201 B

After

Width:  |  Height:  |  Size: 201 B

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="3" width="10" height="2" rx="1" fill="black" />
<rect x="3" y="7" width="10" height="2" rx="1" fill="black" />
<rect x="3" y="11" width="10" height="2" rx="1" fill="black" />
</svg>

After

Width:  |  Height:  |  Size: 298 B

View File

Before

Width:  |  Height:  |  Size: 735 B

After

Width:  |  Height:  |  Size: 735 B

View File

Before

Width:  |  Height:  |  Size: 421 B

After

Width:  |  Height:  |  Size: 421 B

View File

Before

Width:  |  Height:  |  Size: 229 B

After

Width:  |  Height:  |  Size: 229 B

View File

Before

Width:  |  Height:  |  Size: 784 B

After

Width:  |  Height:  |  Size: 784 B

View File

Before

Width:  |  Height:  |  Size: 440 B

After

Width:  |  Height:  |  Size: 440 B

View File

Before

Width:  |  Height:  |  Size: 413 B

After

Width:  |  Height:  |  Size: 413 B

View File

Before

Width:  |  Height:  |  Size: 603 B

After

Width:  |  Height:  |  Size: 603 B

Some files were not shown because too many files have changed in this diff Show More