Merge branch 'develop' into wip/frizi/bazel
@ -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
@ -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
@ -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
|
||||
|
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -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.
|
||||
|
26
.github/workflows/engine-nightly.yml
vendored
@ -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:
|
||||
|
138
.github/workflows/enso4igv.yml
vendored
@ -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
|
||||
|
24
.github/workflows/gui.yml
vendored
@ -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 }}
|
||||
|
907
.github/workflows/nightly-tests.yml
vendored
@ -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"
|
24
.github/workflows/release.yml
vendored
@ -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 }}
|
||||
|
18
.github/workflows/scala-new.yml
vendored
@ -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
@ -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/
|
||||
|
@ -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
@ -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}"
|
||||
}
|
||||
]
|
||||
}
|
4
.vscode/settings.json
vendored
@ -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
@ -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).
|
2103
CHANGELOG.md
656
Cargo.lock
generated
25
Cargo.toml
@ -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" }
|
||||
|
@ -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")
|
||||
#################################
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
14
README.md
@ -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
|
||||
|
@ -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
|
@ -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 {}`.
|
@ -22,6 +22,8 @@ module.exports = {
|
||||
'',
|
||||
'^enso-',
|
||||
'',
|
||||
'^#[/]assets',
|
||||
'',
|
||||
'^#[/]App',
|
||||
'^#[/]appUtils',
|
||||
'^#[/]text',
|
@ -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>
|
@ -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.
|
65
app/dashboard/docs/browser_specific_behavior.md
Normal 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`
|
53
app/dashboard/e2e/README.md
Normal 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");
|
||||
}),
|
||||
),
|
||||
);
|
||||
```
|
@ -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
|
||||
})
|
||||
}
|
158
app/dashboard/e2e/actions/BaseActions.ts
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
291
app/dashboard/e2e/actions/DrivePageActions.ts
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
15
app/dashboard/e2e/actions/EditorPageActions.ts
Normal 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))
|
||||
}
|
||||
}
|
40
app/dashboard/e2e/actions/LoginPageActions.ts
Normal 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()
|
||||
}
|
||||
}
|
37
app/dashboard/e2e/actions/NewDataLinkModalActions.ts
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
21
app/dashboard/e2e/actions/PageActions.ts
Normal 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))
|
||||
}
|
||||
}
|
19
app/dashboard/e2e/actions/SetUsernamePageActions.ts
Normal 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)
|
||||
}
|
||||
}
|
16
app/dashboard/e2e/actions/SettingsPageActions.ts
Normal 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))
|
||||
}
|
||||
}
|
29
app/dashboard/e2e/actions/StartModalActions.ts
Normal 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)
|
||||
}
|
||||
}
|
139
app/dashboard/e2e/actions/contextMenuActions.ts
Normal 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()
|
||||
),
|
||||
}
|
||||
}
|
44
app/dashboard/e2e/actions/goToPageActions.ts
Normal 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
|
||||
),
|
||||
}
|
||||
}
|
16
app/dashboard/e2e/actions/openUserMenuAction.ts
Normal 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()
|
||||
)
|
||||
}
|
49
app/dashboard/e2e/actions/userMenuActions.ts
Normal 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
@ -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
|
||||
}
|
77
app/dashboard/e2e/assetPanel.spec.ts
Normal 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()
|
||||
})
|
||||
)
|
@ -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')
|
109
app/dashboard/e2e/assetsTableFeatures.spec.ts
Normal 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)
|
||||
})
|
||||
)
|
188
app/dashboard/e2e/copy.spec.ts
Normal 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[)]/)
|
||||
})
|
||||
)
|
63
app/dashboard/e2e/createAsset.spec.ts
Normal 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))
|
||||
})
|
||||
)
|
15
app/dashboard/e2e/dataLinkEditor.spec.ts
Normal 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)
|
||||
})
|
||||
)
|
50
app/dashboard/e2e/delete.spec.ts
Normal 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)
|
||||
})
|
||||
)
|
44
app/dashboard/e2e/driveView.spec.ts
Normal 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)
|
||||
})
|
||||
)
|
87
app/dashboard/e2e/editAssetName.spec.ts
Normal 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))
|
||||
})
|
69
app/dashboard/e2e/labels.spec.ts
Normal 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()
|
||||
})
|
@ -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
|
25
app/dashboard/e2e/loginLogout.spec.ts
Normal 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()
|
||||
})
|
||||
)
|
@ -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 ===
|
34
app/dashboard/e2e/membersSettings.spec.ts
Normal 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 ?? ''])
|
||||
})
|
94
app/dashboard/e2e/organizationSettings.spec.ts
Normal 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()
|
||||
})
|
21
app/dashboard/e2e/pageSwitcher.spec.ts
Normal 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()
|
||||
})
|
||||
)
|
123
app/dashboard/e2e/signUp.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
@ -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/)
|
15
app/dashboard/e2e/startModal.spec.ts
Normal 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()
|
||||
})
|
||||
)
|
23
app/dashboard/e2e/userMenu.spec.ts
Normal 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/)
|
||||
})
|
||||
)
|
97
app/dashboard/e2e/userSettings.spec.ts
Normal 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()
|
||||
})
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@ -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>
|
@ -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_"
|
||||
}
|
||||
}
|
@ -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,
|
||||
},
|
@ -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
|
||||
}
|
8
app/dashboard/src/TestAppRunner.tsx
Normal 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>
|
||||
}
|
@ -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'
|
||||
}
|
Before Width: | Height: | Size: 503 B After Width: | Height: | Size: 503 B |
Before Width: | Height: | Size: 408 B After Width: | Height: | Size: 408 B |
Before Width: | Height: | Size: 889 B After Width: | Height: | Size: 889 B |
Before Width: | Height: | Size: 913 B After Width: | Height: | Size: 913 B |
Before Width: | Height: | Size: 703 B After Width: | Height: | Size: 703 B |
Before Width: | Height: | Size: 548 B After Width: | Height: | Size: 548 B |
Before Width: | Height: | Size: 261 B After Width: | Height: | Size: 261 B |
Before Width: | Height: | Size: 303 B After Width: | Height: | Size: 303 B |
Before Width: | Height: | Size: 269 B After Width: | Height: | Size: 269 B |
Before Width: | Height: | Size: 454 B After Width: | Height: | Size: 454 B |
Before Width: | Height: | Size: 327 B After Width: | Height: | Size: 327 B |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 998 B After Width: | Height: | Size: 998 B |
Before Width: | Height: | Size: 102 B After Width: | Height: | Size: 102 B |
Before Width: | Height: | Size: 102 B After Width: | Height: | Size: 102 B |
Before Width: | Height: | Size: 201 B After Width: | Height: | Size: 201 B |
5
app/dashboard/src/assets/burger_menu.svg
Normal 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 |
Before Width: | Height: | Size: 735 B After Width: | Height: | Size: 735 B |
Before Width: | Height: | Size: 421 B After Width: | Height: | Size: 421 B |
Before Width: | Height: | Size: 229 B After Width: | Height: | Size: 229 B |
Before Width: | Height: | Size: 784 B After Width: | Height: | Size: 784 B |
Before Width: | Height: | Size: 440 B After Width: | Height: | Size: 440 B |
Before Width: | Height: | Size: 413 B After Width: | Height: | Size: 413 B |
Before Width: | Height: | Size: 603 B After Width: | Height: | Size: 603 B |