From 694718085fdc8c8f40bdbcb8cbea7388c64a3e1e Mon Sep 17 00:00:00 2001 From: Delasie Torkornoo Date: Wed, 11 Dec 2024 15:11:23 -0500 Subject: [PATCH] Dev.del/feat studio e2e (#375) * feat: setting up playwright for studio-web component * test: finalizing studio-web component e2e tests * test: stories for end-to-end testing of the Studio-Web * test: refactored tests into separate files to parallelize execution and reduce runtime test(ci): setup of matrix sharding to deal with test timeout * feat: upload component subscribes to Soundswallower loading event * test(ci): optimizing ci e2e test for studio-web --------- Co-authored-by: Eric Joanis --- .github/workflows/end-to-end-tests.yml | 82 ++++- .gitignore | 2 + .husky/post-install | 1 + package-lock.json | 128 ++++++- package.json | 3 + packages/studio-web/package.json | 9 +- packages/studio-web/playwright.config.ts | 95 ++++++ .../src/app/demo/demo.component.html | 6 +- .../shared/download/download.component.html | 6 +- .../src/app/soundswallower.service.ts | 6 +- .../src/app/upload/upload.component.html | 15 +- .../src/app/upload/upload.component.ts | 5 + packages/studio-web/tests/fixtures/page1.png | Bin 0 -> 2410 bytes packages/studio-web/tests/fixtures/page2.png | Bin 0 -> 2023 bytes .../tests/fixtures/ref/edited/readalong.eaf | 95 ++++++ .../tests/fixtures/ref/edited/readalong.srt | 16 + .../fixtures/ref/edited/readalong.textgrid | 72 ++++ .../tests/fixtures/ref/edited/readalong.vtt | 13 + .../ref/edited/sentence-paragr-date.readalong | 21 ++ .../tests/fixtures/ref/readalong.eaf | 95 ++++++ .../tests/fixtures/ref/readalong.srt | 16 + .../tests/fixtures/ref/readalong.textgrid | 72 ++++ .../tests/fixtures/ref/readalong.vtt | 13 + .../test-sentence-paragraph-page-56k.mp3 | Bin 0 -> 26277 bytes .../tests/studio-web/check-page-1.spec.ts | 66 ++++ .../tests/studio-web/download-elan.spec.ts | 17 + .../tests/studio-web/download-html.spec.ts | 20 ++ .../tests/studio-web/download-praat.spec.ts | 34 ++ .../tests/studio-web/download-srt.spec.ts | 34 ++ .../studio-web/download-web-bundle.spec.ts | 71 ++++ .../tests/studio-web/download-webvtt.spec.ts | 36 ++ .../tests/studio-web/make-read-along.spec.ts | 90 +++++ packages/studio-web/tests/test-cases.md | 318 ++++++++++++++++++ packages/studio-web/tests/test-commands.ts | 133 ++++++++ .../read-along-component/read-along.tsx | 3 +- 35 files changed, 1572 insertions(+), 21 deletions(-) create mode 100644 .husky/post-install create mode 100644 packages/studio-web/playwright.config.ts create mode 100644 packages/studio-web/tests/fixtures/page1.png create mode 100644 packages/studio-web/tests/fixtures/page2.png create mode 100644 packages/studio-web/tests/fixtures/ref/edited/readalong.eaf create mode 100644 packages/studio-web/tests/fixtures/ref/edited/readalong.srt create mode 100644 packages/studio-web/tests/fixtures/ref/edited/readalong.textgrid create mode 100644 packages/studio-web/tests/fixtures/ref/edited/readalong.vtt create mode 100644 packages/studio-web/tests/fixtures/ref/edited/sentence-paragr-date.readalong create mode 100644 packages/studio-web/tests/fixtures/ref/readalong.eaf create mode 100644 packages/studio-web/tests/fixtures/ref/readalong.srt create mode 100644 packages/studio-web/tests/fixtures/ref/readalong.textgrid create mode 100644 packages/studio-web/tests/fixtures/ref/readalong.vtt create mode 100644 packages/studio-web/tests/fixtures/test-sentence-paragraph-page-56k.mp3 create mode 100644 packages/studio-web/tests/studio-web/check-page-1.spec.ts create mode 100644 packages/studio-web/tests/studio-web/download-elan.spec.ts create mode 100644 packages/studio-web/tests/studio-web/download-html.spec.ts create mode 100644 packages/studio-web/tests/studio-web/download-praat.spec.ts create mode 100644 packages/studio-web/tests/studio-web/download-srt.spec.ts create mode 100644 packages/studio-web/tests/studio-web/download-web-bundle.spec.ts create mode 100644 packages/studio-web/tests/studio-web/download-webvtt.spec.ts create mode 100644 packages/studio-web/tests/studio-web/make-read-along.spec.ts create mode 100644 packages/studio-web/tests/test-cases.md create mode 100644 packages/studio-web/tests/test-commands.ts diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml index 651d0454..8f58ac80 100644 --- a/.github/workflows/end-to-end-tests.yml +++ b/.github/workflows/end-to-end-tests.yml @@ -21,9 +21,7 @@ jobs: node-version: 20 - name: Install everything run: npm install - - name: Always test with the latest browserslist db - run: | - npx update-browserslist-db@latest + - name: Ng test for studio-web run: | npx nx build web-component @@ -56,3 +54,81 @@ jobs: npx nx bundle web-component git status git diff --word-diff=porcelain --word-diff-regex=... --color | perl -ple 's/^(\x1b[^ -+]{0,6})? (.{81,})$/$1 . " " . substr($2, 0, 40) . " [... " . (length($2)-80) . " bytes ...] " . substr($2, -40)/ex' + playwright-tests: + name: Run Playwright test-suites + timeout-minutes: 60 + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shardIndex: [1, 2, 3, 4] + shardTotal: [4] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install and run the back-end API, needed for end-to-end testing + run: | + git clone https://github.com/ReadAlongs/Studio + cd Studio + pip install -e . -r requirements.api.txt + ./run-web-api.sh & + # wait for the API to be up + curl --retry 20 --retry-delay 1 --retry-all-errors http://localhost:8000/api/v1/langs + - name: Install everything + run: npm install + - name: Install dependencies + run: npm ci + - name: Run studio-web in the background + run: | + npx nx build web-component + npx nx run-many --targets=serve,serve-fr,serve-es --projects=web-component,studio-web --parallel 6 & + + # wait for the studio web to be up + sleep 100 + curl --retry 20 --retry-delay 30 --retry-all-errors http://localhost:4200 + - name: Run Playwright tests for studio-web + run: | + npx playwright install --with-deps chromium + npx nx e2e studio-web --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + - name: Upload blob report to GitHub Actions Artifacts + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: blob-report-${{ matrix.shardIndex }} + path: packages/studio-web/blob-report + retention-days: 1 + merge-reports: + # Merge reports after playwright-tests, even if some shards have failed + if: ${{ !cancelled() }} + needs: [playwright-tests] + name: "Merge playwright reports" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install everything + run: npm install + - name: Install dependencies + run: npm ci + - name: Install playwright + run: npx playwright install + - name: Download blob reports from GitHub Actions Artifacts + uses: actions/download-artifact@v4 + with: + path: all-blob-reports + pattern: blob-report-* + merge-multiple: true + + - name: Merge into HTML Report + run: npx playwright merge-reports --reporter=html,github ./all-blob-reports + + - name: Upload HTML report + uses: actions/upload-artifact@v4 + with: + name: html-report--attempt-${{ github.run_attempt }} + path: playwright-report + retention-days: 5 diff --git a/.gitignore b/.gitignore index d894bf63..b370de85 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ www *~ **/version.ts packages/web-component/wordpress-plugin/read-along-web-app-loader/js/* +packages/studio-web/playwright-report +**/test-results diff --git a/.husky/post-install b/.husky/post-install new file mode 100644 index 00000000..76ceb88b --- /dev/null +++ b/.husky/post-install @@ -0,0 +1 @@ +npx playwright install --with-deps firefox chromium webkit diff --git a/package-lock.json b/package-lock.json index 1b0ac0e2..744c510c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "jszip": "^3.10.1", "mime": "^4.0.1", "ngx-toastr": "^18.0.0", + "root": "file:", "shepherd.js": "^11.2.0", "soundswallower": "^0.6.3", "standardized-audio-context": "^25.3.70", @@ -45,6 +46,7 @@ "@nx/jest": "18.3.4", "@nx/storybook": "18.3.4", "@nxext/stencil": "^18", + "@playwright/test": "^1.36.0", "@stencil/angular-output-target": "^0.8.4", "@stencil/core": "^4.15.0", "@stencil/sass": "^3.0.11", @@ -73,6 +75,7 @@ "prettier": "^3.2.5", "pretty-quick": "^4.0.0", "ts-jest": "^29", + "ts-node": "^10.9.2", "tsx": "^4.7.3" } }, @@ -6548,6 +6551,50 @@ "node": ">=8" } }, + "node_modules/@nx/js/node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/@nx/linter": { "version": "18.3.4", "resolved": "https://registry.npmjs.org/@nx/linter/-/linter-18.3.4.tgz", @@ -7775,6 +7822,21 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz", + "integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==", + "dev": true, + "dependencies": { + "playwright": "1.48.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.25", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", @@ -11088,9 +11150,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001680", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz", - "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==", + "version": "1.0.30001687", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001687.tgz", + "integrity": "sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ==", "dev": true, "funding": [ { @@ -21843,6 +21905,50 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz", + "integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==", + "dev": true, + "dependencies": { + "playwright-core": "1.48.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz", + "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", @@ -23436,6 +23542,10 @@ "fsevents": "~2.3.2" } }, + "node_modules/root": { + "resolved": "", + "link": true + }, "node_modules/run-async": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", @@ -25366,10 +25476,11 @@ } }, "node_modules/ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -27614,7 +27725,10 @@ }, "packages/studio-web": { "name": "readalong-studio", - "version": "0.0.0" + "version": "0.0.0", + "dependencies": { + "readalong-studio": "file:" + } }, "packages/web-component": { "name": "@readalongs/web-component", diff --git a/package.json b/package.json index bd43b8c6..5e677f4c 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@nx/jest": "18.3.4", "@nx/storybook": "18.3.4", "@nxext/stencil": "^18", + "@playwright/test": "^1.36.0", "@stencil/angular-output-target": "^0.8.4", "@stencil/core": "^4.15.0", "@stencil/sass": "^3.0.11", @@ -43,6 +44,7 @@ "prettier": "^3.2.5", "pretty-quick": "^4.0.0", "ts-jest": "^29", + "ts-node": "^10.9.2", "tsx": "^4.7.3" }, "dependencies": { @@ -62,6 +64,7 @@ "jszip": "^3.10.1", "mime": "^4.0.1", "ngx-toastr": "^18.0.0", + "root": "file:", "shepherd.js": "^11.2.0", "soundswallower": "^0.6.3", "standardized-audio-context": "^25.3.70", diff --git a/packages/studio-web/package.json b/packages/studio-web/package.json index 83378871..d098d9d2 100644 --- a/packages/studio-web/package.json +++ b/packages/studio-web/package.json @@ -9,9 +9,14 @@ "helpme": "echo This project is part of a monorepo managed using nx. Run the targets in project.json using npx nx target studio-web at the root of the monorepo.", "ng": "ng", "test:ng": "ng test", - "test:once": "ng test --watch=false --browsers ChromeHeadlessCI" + "test:once": "ng test --watch=false --browsers ChromeHeadlessCI", + "e2e": "playwright test", + "e2e-ui": "playwright test --ui" }, "private": true, "singleFileBundleVersion": "1.5.2", - "singleFileBundleTimestamp": "2024-11-18+11-19-49" + "singleFileBundleTimestamp": "2024-11-18+11-19-49", + "dependencies": { + "readalong-studio": "file:" + } } diff --git a/packages/studio-web/playwright.config.ts b/packages/studio-web/playwright.config.ts new file mode 100644 index 00000000..8e9c022b --- /dev/null +++ b/packages/studio-web/playwright.config.ts @@ -0,0 +1,95 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + timeout: (process.env.CI ? 25 : 50) * 1000, + testDir: "./tests", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 3, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : 2, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: process.env.CI ? [["blob", { open: "never" }]] : "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://localhost:4200", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + testIdAttribute: "data-test-id", + video: "retain-on-failure", + }, + + /* Configure projects for major browsers */ + /* Only test chromium on CI */ + projects: process.env.CI + ? [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "Mobile Chrome", + use: { ...devices["Pixel 5"] }, + }, + ] + : [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + /* We do not have full webkit support + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + + /* Test against mobile viewports. */ + { + name: "Mobile Chrome", + use: { ...devices["Pixel 5"] }, + }, + /* + { + name: "Mobile Safari", + use: { ...devices["iPhone 12"] }, + }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/packages/studio-web/src/app/demo/demo.component.html b/packages/studio-web/src/app/demo/demo.component.html index da275a25..f0d7136c 100644 --- a/packages/studio-web/src/app/demo/demo.component.html +++ b/packages/studio-web/src/app/demo/demo.component.html @@ -38,22 +38,24 @@ [(ngModel)]="studioService.slots.title" [ngStyle]="{ 'width.ch': studioService.slots.title.length, - 'min-width.ch': 20, + 'min-width.ch': 20 }" style="border: none" placeholder="Enter your title here" slot="read-along-header" + data-test-id="ra-header" /> diff --git a/packages/studio-web/src/app/shared/download/download.component.html b/packages/studio-web/src/app/shared/download/download.component.html index 13dd931b..cd12fef4 100644 --- a/packages/studio-web/src/app/shared/download/download.component.html +++ b/packages/studio-web/src/app/shared/download/download.component.html @@ -1,13 +1,17 @@
Output Format - + {{ format.display }} @@ -97,6 +99,7 @@

Text

matInput i18n-placeholder="Example input text" placeholder="Ex. Hello my name is..." + data-test-id="ras-text-input" >
@@ -115,6 +118,7 @@

Audio

name="inputMethod" aria-label="Input Method" [value]="studioService.inputMethod.audio" + data-test-id="audio-btn-group" > Record type="file" id="updateAudio" accept=".mp3,.wav,.webm,.m4a" + data-test-id="ras-audio-fileselector" />
[color]="recording ? 'warn' : 'primary'" aria-label="Record button" [disabled]="starting_to_record" + data-test-id="ras-audio-recording-btn" > mic Select Language - + {{ lang.names["_"] }} - ({{ lang.code }}) @@ -353,11 +363,12 @@

i18n="Next button" id="next-step" class="mt-4 plausible-event-name=CreateReadalong" - [disabled]="loading" + [disabled]="loading || !isLoaded" mat-raised-button color="primary" type="submit" (click)="nextStep()" + data-test-id="next-step" > Go to the next step! diff --git a/packages/studio-web/src/app/upload/upload.component.ts b/packages/studio-web/src/app/upload/upload.component.ts index c5e8795a..990ab2df 100644 --- a/packages/studio-web/src/app/upload/upload.component.ts +++ b/packages/studio-web/src/app/upload/upload.component.ts @@ -88,6 +88,11 @@ export class UploadComponent implements OnDestroy, OnInit { .subscribe((textString) => this.uploadService.$currentText.next(textString), ); + this.ssjsService.modelLoaded + .pipe(takeUntil(this.unsubscribe$)) + .subscribe((loaded) => { + this.isLoaded = loaded; + }); } async ngOnInit() { diff --git a/packages/studio-web/tests/fixtures/page1.png b/packages/studio-web/tests/fixtures/page1.png new file mode 100644 index 0000000000000000000000000000000000000000..f4b793c09dc1a1851838d316a58b73d8363cfbc5 GIT binary patch literal 2410 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 + +
+ 0 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + This is a test + + + + + Sentences + + + + + Paragraph + + + + + Page + + + + + + + This + + + + + is + + + + + a + + + + + test + + + + + Sentences + + + + + Paragraph + + + + + Page + + + + + + + + +
diff --git a/packages/studio-web/tests/fixtures/ref/edited/readalong.srt b/packages/studio-web/tests/fixtures/ref/edited/readalong.srt new file mode 100644 index 00000000..12657b60 --- /dev/null +++ b/packages/studio-web/tests/fixtures/ref/edited/readalong.srt @@ -0,0 +1,16 @@ +1 +00:00:00,500 --> 00:00:01,800 +This is a test + +2 +00:00:01,800 --> 00:00:02,360 +Sentences + +3 +00:00:02,360 --> 00:00:03,030 +Paragraph + +4 +00:00:03,030 --> 00:00:03,400 +Page + diff --git a/packages/studio-web/tests/fixtures/ref/edited/readalong.textgrid b/packages/studio-web/tests/fixtures/ref/edited/readalong.textgrid new file mode 100644 index 00000000..34b0cc2a --- /dev/null +++ b/packages/studio-web/tests/fixtures/ref/edited/readalong.textgrid @@ -0,0 +1,72 @@ +File type = "ooTextFile" +Object class = "TextGrid" + +xmin = 0.000000 +xmax = 3.400000 +tiers? +size = 2 +item []: + item [1]: + class = "IntervalTier" + name = "Sentence" + xmin = 0.000000 + xmax = 3.400000 + intervals: size = 5 + intervals [1]: + xmin = 0.000000 + xmax = 0.500000 + text = "" + intervals [2]: + xmin = 0.500000 + xmax = 1.800000 + text = "This is a test" + intervals [3]: + xmin = 1.800000 + xmax = 2.360000 + text = "Sentences" + intervals [4]: + xmin = 2.360000 + xmax = 3.030000 + text = "Paragraph" + intervals [5]: + xmin = 3.030000 + xmax = 3.400000 + text = "Page" + item [2]: + class = "IntervalTier" + name = "Word" + xmin = 0.000000 + xmax = 3.400000 + intervals: size = 8 + intervals [1]: + xmin = 0.000000 + xmax = 0.500000 + text = "" + intervals [2]: + xmin = 0.500000 + xmax = 1.070000 + text = "This" + intervals [3]: + xmin = 1.070000 + xmax = 1.200000 + text = "is" + intervals [4]: + xmin = 1.200000 + xmax = 1.230000 + text = "a" + intervals [5]: + xmin = 1.230000 + xmax = 1.800000 + text = "test" + intervals [6]: + xmin = 1.800000 + xmax = 2.360000 + text = "Sentences" + intervals [7]: + xmin = 2.360000 + xmax = 3.030000 + text = "Paragraph" + intervals [8]: + xmin = 3.030000 + xmax = 3.400000 + text = "Page" diff --git a/packages/studio-web/tests/fixtures/ref/edited/readalong.vtt b/packages/studio-web/tests/fixtures/ref/edited/readalong.vtt new file mode 100644 index 00000000..881a4035 --- /dev/null +++ b/packages/studio-web/tests/fixtures/ref/edited/readalong.vtt @@ -0,0 +1,13 @@ +WEBVTT + +00:00:00.500 --> 00:00:01.800 +This is a test + +00:00:01.800 --> 00:00:02.360 +Sentences + +00:00:02.360 --> 00:00:03.030 +Paragraph + +00:00:03.030 --> 00:00:03.400 +Page diff --git a/packages/studio-web/tests/fixtures/ref/edited/sentence-paragr-date.readalong b/packages/studio-web/tests/fixtures/ref/edited/sentence-paragr-date.readalong new file mode 100644 index 00000000..5a386e4d --- /dev/null +++ b/packages/studio-web/tests/fixtures/ref/edited/sentence-paragr-date.readalong @@ -0,0 +1,21 @@ + + + + +
+

+ This is a test.Un vrai test. + Sentences.Phrase. +

+

+ Paragraph. +

+
+
+

+ Page. +

+
+ +
+
diff --git a/packages/studio-web/tests/fixtures/ref/readalong.eaf b/packages/studio-web/tests/fixtures/ref/readalong.eaf new file mode 100644 index 00000000..1e0c31ce --- /dev/null +++ b/packages/studio-web/tests/fixtures/ref/readalong.eaf @@ -0,0 +1,95 @@ + + +
+ 0 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + This is a test + + + + + Sentence + + + + + Paragraph + + + + + Page + + + + + + + This + + + + + is + + + + + a + + + + + test + + + + + Sentence + + + + + Paragraph + + + + + Page + + + + + + + + +
diff --git a/packages/studio-web/tests/fixtures/ref/readalong.srt b/packages/studio-web/tests/fixtures/ref/readalong.srt new file mode 100644 index 00000000..70dad4c1 --- /dev/null +++ b/packages/studio-web/tests/fixtures/ref/readalong.srt @@ -0,0 +1,16 @@ +1 +00:00:00,840 --> 00:00:01,800 +This is a test + +2 +00:00:01,800 --> 00:00:02,360 +Sentence + +3 +00:00:02,360 --> 00:00:03,030 +Paragraph + +4 +00:00:03,030 --> 00:00:03,400 +Page + diff --git a/packages/studio-web/tests/fixtures/ref/readalong.textgrid b/packages/studio-web/tests/fixtures/ref/readalong.textgrid new file mode 100644 index 00000000..c08343ec --- /dev/null +++ b/packages/studio-web/tests/fixtures/ref/readalong.textgrid @@ -0,0 +1,72 @@ +File type = "ooTextFile" +Object class = "TextGrid" + +xmin = 0.000000 +xmax = 3.400000 +tiers? +size = 2 +item []: + item [1]: + class = "IntervalTier" + name = "Sentence" + xmin = 0.000000 + xmax = 3.400000 + intervals: size = 5 + intervals [1]: + xmin = 0.000000 + xmax = 0.840000 + text = "" + intervals [2]: + xmin = 0.840000 + xmax = 1.800000 + text = "This is a test" + intervals [3]: + xmin = 1.800000 + xmax = 2.360000 + text = "Sentence" + intervals [4]: + xmin = 2.360000 + xmax = 3.030000 + text = "Paragraph" + intervals [5]: + xmin = 3.030000 + xmax = 3.400000 + text = "Page" + item [2]: + class = "IntervalTier" + name = "Word" + xmin = 0.000000 + xmax = 3.400000 + intervals: size = 8 + intervals [1]: + xmin = 0.000000 + xmax = 0.840000 + text = "" + intervals [2]: + xmin = 0.840000 + xmax = 1.070000 + text = "This" + intervals [3]: + xmin = 1.070000 + xmax = 1.200000 + text = "is" + intervals [4]: + xmin = 1.200000 + xmax = 1.230000 + text = "a" + intervals [5]: + xmin = 1.230000 + xmax = 1.800000 + text = "test" + intervals [6]: + xmin = 1.800000 + xmax = 2.360000 + text = "Sentence" + intervals [7]: + xmin = 2.360000 + xmax = 3.030000 + text = "Paragraph" + intervals [8]: + xmin = 3.030000 + xmax = 3.400000 + text = "Page" diff --git a/packages/studio-web/tests/fixtures/ref/readalong.vtt b/packages/studio-web/tests/fixtures/ref/readalong.vtt new file mode 100644 index 00000000..ca0e5f1a --- /dev/null +++ b/packages/studio-web/tests/fixtures/ref/readalong.vtt @@ -0,0 +1,13 @@ +WEBVTT + +00:00:00.840 --> 00:00:01.800 +This is a test + +00:00:01.800 --> 00:00:02.360 +Sentence + +00:00:02.360 --> 00:00:03.030 +Paragraph + +00:00:03.030 --> 00:00:03.400 +Page diff --git a/packages/studio-web/tests/fixtures/test-sentence-paragraph-page-56k.mp3 b/packages/studio-web/tests/fixtures/test-sentence-paragraph-page-56k.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..051998b252464205b52a985c5a0dcbe84f6b22a1 GIT binary patch literal 26277 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%0AeSzD^8R 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 { + test("should check UI (en)", async ({ page }) => { + await page.goto("/"); + //tour button is visible + await expect(page.getByText("Take the tour!")).toBeVisible(); + //check text button group + await expect(page.getByTestId("text-btn-group")).toBeVisible(); + //check audio button group + await expect(page.getByTestId("audio-btn-group")).toBeVisible(); + //check the language list + await expect(page.getByTestId("language-list")).toBeDisabled(); + await page + .getByRole("radio", { name: "Select a specific language" }) + .check(); + await expect(page.getByTestId("language-list")).toBeEnabled(); + }); + test("should check UI (fr)", async ({ page }) => { + await page.goto("http://localhost:4203/"); + //tour button is visible + await expect( + page.getByRole("button", { name: "Visite guidée" }), + ).toBeVisible(); + //check text button group + await expect(page.getByTestId("text-btn-group")).toBeVisible(); + //check audio button group + await expect(page.getByTestId("audio-btn-group")).toBeVisible(); + //check the language list + await expect(page.getByTestId("language-list")).toBeDisabled(); + await page + .getByRole("radio", { name: "Sélectionner une languge spécifique" }) + .check(); + await expect(page.getByTestId("language-list")).toBeEnabled(); + }); + test("should check UI (es)", async ({ page }) => { + await page.goto("http://localhost:4204/"); + //tour button is visible + await expect(page.getByText("¡Siga el tour!")).toBeVisible(); + //check text button group + await expect(page.getByTestId("text-btn-group")).toBeVisible(); + //check audio button group + await expect(page.getByTestId("audio-btn-group")).toBeVisible(); + //check the language list + await expect(page.getByTestId("language-list")).toBeDisabled(); + await page + .getByRole("radio", { name: "Seleccione un idioma específico" }) + .check(); + await expect(page.getByTestId("language-list")).toBeEnabled(); + }); + test("should input and save text", async ({ page }) => { + await page.goto("/"); + await expect(page.getByTestId("text-download-btn")).toBeDisabled(); + await page.getByTestId("ras-text-input").fill(testText); + await expect(page.getByTestId("text-download-btn")).toBeEnabled(); + + const download2Promise = page.waitForEvent("download"); + await page.getByTestId("text-download-btn").click(); + const download2 = await download2Promise; + await expect( + download2.suggestedFilename(), + "should have the expected filename", + ).toMatch(/ras-text-\d+\.txt/); + }); +}); diff --git a/packages/studio-web/tests/studio-web/download-elan.spec.ts b/packages/studio-web/tests/studio-web/download-elan.spec.ts new file mode 100644 index 00000000..2c4af2d0 --- /dev/null +++ b/packages/studio-web/tests/studio-web/download-elan.spec.ts @@ -0,0 +1,17 @@ +import { test, expect } from "@playwright/test"; +import { testMakeAReadAlong, defaultBeforeEach } from "../test-commands"; + +test("should Download ELAN ( file format)", async ({ page, browserName }) => { + await defaultBeforeEach(page, browserName); + await testMakeAReadAlong(page); + + await page.locator("#mat-select-value-3").click(); + await page.getByRole("option", { name: "Elan File" }).click(); + const download2Promise = page.waitForEvent("download"); + await page.getByTestId("download-ras").click(); + const download2 = await download2Promise; + await expect( + download2.suggestedFilename(), + "should have the expected filename", + ).toMatch(/readalong\.eaf/); +}); diff --git a/packages/studio-web/tests/studio-web/download-html.spec.ts b/packages/studio-web/tests/studio-web/download-html.spec.ts new file mode 100644 index 00000000..7f392d52 --- /dev/null +++ b/packages/studio-web/tests/studio-web/download-html.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from "@playwright/test"; +import { testMakeAReadAlong, defaultBeforeEach } from "../test-commands"; + +test("should Download default (single file format)", async ({ + page, + browserName, +}) => { + await expect(async () => { + await defaultBeforeEach(page, browserName); + await testMakeAReadAlong(page); + }).toPass(); + //download default + const downloadPromise = page.waitForEvent("download"); + await page.getByTestId("download-ras").click(); + const download = await downloadPromise; + await expect( + download.suggestedFilename(), + "should have the expected filename", + ).toMatch(/sentence\-paragr\-[0-9]*\.html/); +}); diff --git a/packages/studio-web/tests/studio-web/download-praat.spec.ts b/packages/studio-web/tests/studio-web/download-praat.spec.ts new file mode 100644 index 00000000..d30ce846 --- /dev/null +++ b/packages/studio-web/tests/studio-web/download-praat.spec.ts @@ -0,0 +1,34 @@ +import { test, expect } from "@playwright/test"; +import { + testAssetsPath, + testMakeAReadAlong, + defaultBeforeEach, + replaceValuesWithZeroes, +} from "../test-commands"; +import fs from "fs"; + +test("should Download Praat ( file format)", async ({ page, browserName }) => { + await defaultBeforeEach(page, browserName); + await testMakeAReadAlong(page); + + await page.locator("#mat-select-value-3").click(); + await page.getByRole("option", { name: "Praat TextGrid" }).click(); + const download2Promise = page.waitForEvent("download"); + await page.getByTestId("download-ras").click(); + const download2 = await download2Promise; + await expect( + download2.suggestedFilename(), + "should have the expected filename", + ).toMatch(/readalong\.textgrid/); + /* check output*/ + const filePath = await download2.path(); + const fileData = fs.readFileSync(filePath, { encoding: "utf8", flag: "r" }); + const refFileData = fs.readFileSync( + `${testAssetsPath}/ref/readalong.textgrid`, + { encoding: "utf8", flag: "r" }, + ); + await expect( + replaceValuesWithZeroes(fileData.replace(/\r/g, "")), + "file content should match reference data", + ).toEqual(replaceValuesWithZeroes(refFileData)); +}); diff --git a/packages/studio-web/tests/studio-web/download-srt.spec.ts b/packages/studio-web/tests/studio-web/download-srt.spec.ts new file mode 100644 index 00000000..7af1a2f1 --- /dev/null +++ b/packages/studio-web/tests/studio-web/download-srt.spec.ts @@ -0,0 +1,34 @@ +import { test, expect } from "@playwright/test"; +import { + testAssetsPath, + testMakeAReadAlong, + defaultBeforeEach, + replaceValuesWithZeroes, +} from "../test-commands"; +import fs from "fs"; + +test("should Download SRT ( file format)", async ({ page, browserName }) => { + await defaultBeforeEach(page, browserName); + await testMakeAReadAlong(page); + + await page.locator("#mat-select-value-3").click(); + await page.getByRole("option", { name: "SRT Subtitles" }).click(); + const download2Promise = page.waitForEvent("download"); + await page.getByTestId("download-ras").click(); + const download2 = await download2Promise; + await expect( + download2.suggestedFilename(), + "should have the expected filename", + ).toMatch(/readalong\.srt/); + + const filePath = await download2.path(); + const fileData = fs.readFileSync(filePath, { encoding: "utf8", flag: "r" }); + const refFileData = fs.readFileSync(`${testAssetsPath}/ref/readalong.srt`, { + encoding: "utf8", + flag: "r", + }); + await expect( + replaceValuesWithZeroes(fileData.replace(/\r/g, "")), + "file content should match reference data", + ).toEqual(replaceValuesWithZeroes(refFileData)); +}); diff --git a/packages/studio-web/tests/studio-web/download-web-bundle.spec.ts b/packages/studio-web/tests/studio-web/download-web-bundle.spec.ts new file mode 100644 index 00000000..dc9708f8 --- /dev/null +++ b/packages/studio-web/tests/studio-web/download-web-bundle.spec.ts @@ -0,0 +1,71 @@ +import { test, expect } from "@playwright/test"; +import { testMakeAReadAlong, defaultBeforeEach } from "../test-commands"; +import fs from "fs"; +import JSZip from "jszip"; + +test("should Download web bundle (zip file format)", async ({ + page, + browserName, +}) => { + test.slow(); + + await defaultBeforeEach(page, browserName); + await testMakeAReadAlong(page); + + //download web bundle + await page.getByLabel("2Step").locator("svg").click(); + await page.locator(".cdk-overlay-backdrop").click(); + await page.locator("#mat-select-value-3").click(); + await page.getByRole("option", { name: "Web Bundle" }).click(); + const download1Promise = page.waitForEvent("download"); + await page.getByTestId("download-ras").click(); + const download1 = await download1Promise; + await expect( + download1.suggestedFilename(), + "should have the expected filename", + ).toMatch(/sentence\-paragr\-[0-9]*\.zip/); + //await download1.saveAs(testAssetsPath + download1.suggestedFilename()); + const zipPath = await download1.path(); + const zipBin = await fs.readFileSync(zipPath); + const zip = await JSZip.loadAsync(zipBin); + await expect( + zip.folder(/Offline-HTML/), + "should have Offline-HTML folder", + ).toHaveLength(1); //Offline-HTML folder exists + await expect( + zip.file(/Offline-HTML\/sentence\-paragr\-[0-9]*\.html/), + "should have Offline-HTML file", + ).toHaveLength(1); //Offline-HTML folder exists + await expect( + zip.folder(/www/).length, + "should have www folder", + ).toBeGreaterThan(1); //www folder exists + await expect( + zip.folder(/www\/assets/), + "should have assets folder", + ).toHaveLength(1); //www/assets folder exists + await expect( + zip.file(/www\/assets\/sentence\-paragr\-[0-9]*\.readalong/), + "should have readalong file", + ).toHaveLength(1); //www/assets readalong exists + await expect( + zip.file(/www\/assets\/sentence\-paragr\-[0-9]*\.wav/), + "should have wav file", + ).toHaveLength(1); //www/assets audio exists + await expect( + zip.file(/www\/assets\/image-sentence\-paragr\-[0-9\-]*\.png/), + "should have image files", + ).toHaveLength(2); //www/assets image exists + await expect( + zip.file(/www\/sentence\-paragr\-[0-9]*\.txt/), + "should have readalong plain text file", + ).toHaveLength(1); //www/ readalong text exists + await expect( + zip.file(/www\/readme.txt/), + "should have readme file", + ).toHaveLength(1); //www/ readme text exists + await expect( + zip.file(/www\/index.html/), + "should have index file", + ).toHaveLength(1); //www/index.html exists +}); diff --git a/packages/studio-web/tests/studio-web/download-webvtt.spec.ts b/packages/studio-web/tests/studio-web/download-webvtt.spec.ts new file mode 100644 index 00000000..68f05f79 --- /dev/null +++ b/packages/studio-web/tests/studio-web/download-webvtt.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from "@playwright/test"; +import { + testMakeAReadAlong, + defaultBeforeEach, + testAssetsPath, + replaceValuesWithZeroes, +} from "../test-commands"; +import fs from "fs"; + +test("should Download WebVTT ( file format)", async ({ page, browserName }) => { + test.slow(); + + await defaultBeforeEach(page, browserName); + await testMakeAReadAlong(page); + + await page.locator("#mat-select-value-3").click(); + await page.getByRole("option", { name: "WebVTT Subtitles" }).click(); + const download2Promise = page.waitForEvent("download"); + await page.getByTestId("download-ras").click(); + const download2 = await download2Promise; + await expect( + download2.suggestedFilename(), + "should have the expected filename", + ).toMatch(/readalong\.vtt/); + // check output + const filePath = await download2.path(); + const fileData = fs.readFileSync(filePath, { encoding: "utf8", flag: "r" }); + const refFileData = fs.readFileSync(`${testAssetsPath}/ref/readalong.vtt`, { + encoding: "utf8", + flag: "r", + }); + await expect( + replaceValuesWithZeroes(refFileData), + "file content should match reference data", + ).toMatch(replaceValuesWithZeroes(fileData.replace(/\r/g, ""))); +}); diff --git a/packages/studio-web/tests/studio-web/make-read-along.spec.ts b/packages/studio-web/tests/studio-web/make-read-along.spec.ts new file mode 100644 index 00000000..aa0682d5 --- /dev/null +++ b/packages/studio-web/tests/studio-web/make-read-along.spec.ts @@ -0,0 +1,90 @@ +import { test, expect } from "@playwright/test"; +import { + testText, + testMp3Path, + testAssetsPath, + defaultBeforeEach, +} from "../test-commands"; + +test("should make read along", async ({ page, browserName }) => { + test.slow(); + await defaultBeforeEach(page, browserName); + //fill in text and audio + await page.getByTestId("ras-text-input").fill(testText); + await expect(page.getByTestId("text-download-btn")).toBeVisible(); + await page + .getByTestId("audio-btn-group") + .getByRole("button", { name: "File" }) + .click(); + await page.getByTestId("ras-audio-fileselector").click(); + await page.getByTestId("ras-audio-fileselector").setInputFiles(testMp3Path); + await expect(page.getByLabel("Play")).toBeVisible(); + await expect(page.getByLabel("Audio save button")).toBeVisible(); + await expect(page.getByLabel("Delete")).toBeVisible(); + //create the readalong + await page.getByTestId("next-step").click(); + //edit the headers + await expect(page.getByTestId("ra-header")).toHaveValue("Title"); + await expect(page.getByTestId("ra-header")).toBeEditable(); + await page.getByTestId("ra-header").dblclick(); + await page.getByTestId("ra-header").fill("Sentence Paragraph Page"); + await expect(page.getByTestId("ra-header")).toHaveValue( + "Sentence Paragraph Page", + ); + await expect(page.getByTestId("ra-subheader")).toHaveValue("Subtitle"); + await expect(page.getByTestId("ra-subheader")).toBeEditable(); + await page.getByTestId("ra-subheader").click(); + await page.getByTestId("ra-subheader").dblclick(); + await page.getByTestId("ra-subheader").fill("by me"); + await expect(page.getByTestId("ra-subheader")).toHaveValue("by me"); + //add translations + await expect( + page.locator("#t0b0d0p0s0").getByLabel("Add translation"), + ).toBeVisible(); + await page.locator("#t0b0d0p0s0").getByRole("button").click(); + await expect( + page.locator("#t0b0d0p0s0").getByLabel("Remove translation"), + ).toBeVisible(); + await expect( + page.locator("#t0b0d0p0s0").getByLabel("Add translation"), + ).toBeHidden(); //check + await page.locator("#t0b0d0p0s1").getByRole("button").click(); + await page.locator("#t0b0d0p1s0").getByRole("button").click(); + //update translations + let translation = await page.locator("#t0b0d0p0s0translation"); + await translation.click(); + await expect(translation).toBeEditable(); + await translation.fill("Ceci est un test."); + + translation = await page.locator("#t0b0d0p0s1translation"); + await translation.click(); + await translation.fill("Phrase."); + + translation = await page.locator("#t0b0d0p1s0translation"); + await translation.click(); + await translation.fill("Paragraphe."); + await expect(page.locator(".editable__translation")).toHaveCount(3); + // delete a translation + await page.locator("#t0b0d0p0s1").getByRole("button").click(); + await expect(page.locator(".editable__translation")).toHaveCount(2); + await page.locator("#t0b0d0p1s0").getByRole("button").click(); + await expect(page.locator(".editable__translation").first()).toBeVisible(); + await page.getByTestId("translation-toggle").click(); + await expect(page.locator(".editable__translation").first()).toBeHidden(); + //upload a photo to page 1 + let fileChooserPromise = page.waitForEvent("filechooser"); + page.locator("#fileElem--t0b0d0").dispatchEvent("click"); + + let fileChooser = await fileChooserPromise; + fileChooser.setFiles(testAssetsPath + "page1.png"); + //delete the photo uploaded + await page.getByTestId("delete-button").click(); + await page.locator("#t0b0d0p0s0w0").click(); + //upload a photo to page 2 + fileChooserPromise = page.waitForEvent("filechooser"); + page.locator("#fileElem--t0b0d1").dispatchEvent("click"); + + fileChooser = await fileChooserPromise; + fileChooser.setFiles(testAssetsPath + "page2.png"); + await page.getByRole("tab", { name: "Editable Step" }).click(); +}); diff --git a/packages/studio-web/tests/test-cases.md b/packages/studio-web/tests/test-cases.md new file mode 100644 index 00000000..b974b1ae --- /dev/null +++ b/packages/studio-web/tests/test-cases.md @@ -0,0 +1,318 @@ +# Test cases for end-to-end testing of the ReadAlong Studio-Web Studio and Editor + +Global setting: make sure analytics is disable, stubbed out, or sent to a fake +server, so we don't generate bogus traffic. + +## Story 1: main walk through the Studio + +### Fill in Step 1 + +- Launch Studio. + - Expect it is loaded. +- Enter this text in the text box. + + This is a test. + Sentence. + + Paragraph. + + + Page. + +- Load this audio: `test-sentence-paragraph-page-56k.mp3`. +- Click on "Go to the next step!". + - Expect alignment to succeed, and step 2 to get displayed with the preview. + +### Edit the read along in Step 2 + +- Click on Title and type "Sentence Paragraph Page". +- Click on Subtitle and type "by me". +- Click in turn on the first and second "+" buttons and add translations "Ceci est un test." + and "Phrase.", respectively. +- Add translation "Paragraphe." to the 3rd sentence. +- Click "-" to delete the translation of the 2nd sentence, "Sentence." +- Click on the translation toggle. + - Expect all translations to be hidden. +- Click on the translation toggle again. + - Expect translations for the 1st and 3rd sentences to be visible. +- Click to add an image on page 1: add `page1.png` + - Expect the image is visible and the image delete button appears +- Click on Delete to remove the image + - Expect the option to choose a file is back +- Scroll to page two and add `page2.png`. + +### Download formats + +- Click on the download button + - Expect a file download to happen +- Let download1 = name of that downloaded file. + - Expect download1 matches RE `sentence-paragr-[0-9]*.html` + - TODO validate the contents, somehow +- Select Web Bundle output format +- Click on the download button + - Expect a file download to happen +- Let download2 = name of that downloaded zip file. + + - Expect download2 matches RE `sentence-paragr-[0-9]*.zip` + - Expect download2 contents to look like this: + + $ unzip -l sentence-paragr-20241023184608.zip + Archive: sentence-paragr-20241023184608.zip + Length Date Time Name + --------- ---------- ----- ---- + 0 2024-10-23 18:46 www/ + 0 2024-10-23 18:46 Offline-HTML/ + 0 2024-10-23 18:46 www/assets/ + 865623 2024-10-23 18:46 Offline-HTML/sentence-paragr-20241023184608.html + 26277 2024-10-23 18:46 www/assets/sentence-paragr-20241023184608.wav + 2023 2024-10-23 18:46 www/assets/image-sentence-paragr-20241023184608-1.png + 45 2024-10-23 18:46 www/sentence-paragr-20241023184608.txt + 1749 2024-10-23 18:46 www/assets/sentence-paragr-20241023184608.readalong + 1378 2024-10-23 18:46 www/index.html + 1750 2024-10-23 18:46 www/readme.txt + --------- ------- + 898845 10 files + + We can't check this exactly, but maybe all the filenames, and some contents: + + - `Offline-HTML/sentence-paragr-.html` is identical to download1. + - `www/assets/image-sentence-paragr--1.png` is identical to `page1.png`. + - `www/assets/sentence-paragr-.wav` is identical to `test-sentence-paragraph-page-56k.mp3`. + - `www/index.html` is identical to `ref/www/index.html` except for the + dates, and we'll want soft updating the version number so we don't have to + change the ref files for each version bump + - `www/readme.txt` is identical to `ref/www/readme.txt` modulo date and version number. + - `www/sentence-paragr-.txt` is identical to `ref/www/sentence-paragr-date.txt`. + - `www/assets/sentence-paragr-.readalong` is identical to + `ref/www/assets/sentence-paragr-date.readalong` modulo date (for the + .png file) and time= and dur= values for each word. And maybe get the + read-along version number from config and studio-cli version number too, + so the test doesn't fail whenever those get bumped. + +- For each of the other four download format: + - select it + - click download + - check the download contents against the matching file in `ref`. + - Note: those files are all called `readalong.`, should probably also be `-<date>.<ext>` (#366) + +## Story 2: main walk through the Editor + +### Load a readalong to edit + +- Launch the Editor +- load download1 from story 1 + - Expect the readalong shown + - Expect no image on page 1, an image on page 2 + - Expect translations for the 1st and 3rd sentence + - Expect no translation for the second sentence + +### edit images + +- add image `page1.png` to page 1 + - expect to see it +- remove the image on page 2 + - expect the add image button to be back on page 2 + +### edit translations + +- edit the translation of the first sentence to say "Un vrai test." +- click + to add translation "Phrase." to the second sentence +- click - to delete the translation of the third sentence + - Expect to see translations for the 1st and 2nd sentence but not the third +- click on translation toggle + - Expect translations to be hidden +- click on translation toggle + - Expect to see translations for the 1st and 2nd sentence but not the third + +### Change an alignment + +- click on "This" in the web-c preview + - Expect the audio playback cue to be betwee 0.7 s and 1.0 s in the web-c preview +- Drag the start of "This" to 0.5 s in the Audio Toolbar +- click on "This" in the web-c preview + - Expect the audio playback cue to be at 0.5 s in the web-c preview + +### Change a word + +- Before: + - Expect the second sentence's word in the web-c to read "Sentence" +- click on "Sentence" in the Audio Toolbar +- add an "s" at the end of the word so it spells "Sentences" + - Expect the second sentence's word in the web-c to read "Sentences" with an "s" added + +### Download the results + +- Select Web Bundle format +- Click download + + - Expect the .readalong file in the Zip file to match my hand-created `ref/sentence-paragr-edited.readalong` modulo dates. + - Expect the timing for "This" to be close to `time="0.50" dur="0.57"` (rather than the original `time="0.84" dur="0.23"`) + - Expect `<w id="t0b0d0p0s1w0"` to be spelled "Sentences" with an "s" + - Expect `<graphic url="image-sentence-paragr-20241023184608-0.png"/>` on `<div type="page" id="t0b0d0">` + - Expect no graphic element on `<div type="page" id="t0b0d1">` + - Expect translations on the first and second sentences + - Expect no translations on the 3rd and 4th sentences. + +- Select the Offline HTML format +- click download + + - Expect a .html file + +- Select each of the remaining four formats and download them + - Expect each to match the corresponding ref file in `ref/edited`. + - Specifically, in each format, check that + - "This" starts at 0.5s + - "Sentences" has been updated to take the plural "s". + +## Story 3: run the Tour + +### Take the tour from the top right through to the end + +- Launch the Studio +- click on "Take the Tour" + - expect it to start +- click next until "That's it!" is there +- click "Next (overwrites your data)" + - Expect Step 2 to load and the tour to continue +- click next until "Go to the Editor" is there +- click "Editor" (or "To the Editor" once #349 is merged) + - Expect the Editor to open and the tour to continue +- click "Next" until the last step, with "Close" +- click "Close" + - Expect to be in the Editor with the dummy "Hello world!" RA loaded. + +### Take the tour from the Editor + +- We are still in the Editor at this point. +- Click "Take the Tour". + - Expect the Editor tour to start. +- Click through the tour. + +## Story 4: Analytics + +Question: can we stub analytics? I'd like to detect that analytics work without +causing events to be send to Plausible. Maybe we can give it a bogus hostname +and track network traffic? We really don't want these tests to trigger traffic +on Plausible! + +### Turn analytics on and off + +- Launch Studio +- click on Privacy (will depend on screen size with #349) + - Expect the Privacy Policy to be displayed +- click on "Opt out of Analytics" if shown (i.e., unless "Opt in to Analytics" is shown) +- click on "I agree" +- Click Take the Tour + - Expect no event sent to analytics +- type Escape to exit the tour +- Reload the page + + - Expect no event sent to analytics + +- Opt in to analytics +- Click Take the Tour + - Expect a Tour event sent to analytics +- Reload the page + + - Expect a Studio event sent to analytics + +- Load text and audio as in Story 1 +- click "Go to the next step!" + - Expect step 2 to show + - Expect a CreateReadalong event sent to analytics +- click on the Download icon + + - Expect a Download event sent to analytics + +- click on "Step 1" + - Expect step 1 to be shown again +- opt out of analytics +- click on "Go to the next step!" + - Expect no event sent to analytics +- click on the Download icon + - Expect no event sent to analytics + +## Story 5: click around the Studio a lot + +- Load the Studio + +### Text entry + +- Click on "? Format" in Text entry + - Expect the "Here is how to format your plain text input text." modal to show +- click on Close +- click on "Go to the next step" + - Expect three error toasts, including "No text" +- Enter "Random Text" in the text box +- Click on "Save a copy" + - Expect a `ras-text-<date>.txt` file to be downloaded containing "Random Text" +- Click on File + - Expect the Choose file option to appear +- click on Choose file and select `ref/www/sentence-paragr-date.txt` + - Can't expect anything here: the contents never get shown in step 1 we should + probably fix that, somehow. + +### Audio + +- Expect to see only the "Record" button +- Click on File + - Expect the Choose File option +- choose `test-sentence-paragraph-page-56k.mp3` + - Expect the "Play", "Save a copy" and "Delete" buttons to be shown +- Click on Record + - Expect the "Delete and re-record", "Play", "Save a copy" and "Delete" buttons to be shown +- Click on Delete + - Expect to see only the "Record" button" +- click on Go to the next step! + - Expect two error toasts, including "No audio" +- click on File and reload the same audio file. + +### Language settings + +- Expect Default is selected +- Expect Select Language drop down to be shown but inactive +- click Select a specific language + - Expect Select Language drop down to be active +- click "Go the the next step!" + - Expect the "No language selected" error toast +- Select English + +### Go to step 2 + +- click on Go to the next step! + + - Expect the "Great!" success toast + - Expect step 2 to load + - Expect Title and Subtitle to be "Title" and "Subtitle" + +- Edit Title and Subtitle to "foo" and "bar" + +- click on Step 1 (top left) + + - Expect step 1 to display again with the same text file, audio file and English still selected + +- click on Step 2 (top right) + + - Expect step 2 to display, but no toast + - Expect Title and Subtitle to still be "foo" and "bar". + +## Story 6: click around the Editor + +- Load the Editor + +- Load download1 + +- In the Audio Toolbar, click on Zoom + twice + + - Expect a zoomed in view showing a shorter duration than the whole + +- click on the Zoom - five times (three times is enough on a large screen) + - Expect the zoomed out view with the whole text and no scrool bar anymore + +## Notes and Questions + +I assume the web-component testing already verifies play/pause/back 5 sec, +etc, so I'm not covering them here. + +Q: Does the web-c testing already test the gear menu for setting My Preferences? +That also doesn't really belong here, but needs to happen somewhere. diff --git a/packages/studio-web/tests/test-commands.ts b/packages/studio-web/tests/test-commands.ts new file mode 100644 index 00000000..d59cf44c --- /dev/null +++ b/packages/studio-web/tests/test-commands.ts @@ -0,0 +1,133 @@ +import { test, expect, Page } from "@playwright/test"; +import process from "process"; + +export const testAssetsPath = process.cwd().includes("packages") + ? "tests/fixtures/" // for nx + : "packages/studio-web/tests/fixtures/"; //for vscode +export const testText = `This is a test. +Sentence. + +Paragraph. + + +Page.`; +export const testMp3Path = + testAssetsPath + "test-sentence-paragraph-page-56k.mp3"; +/** + * Steps to recreate a readalong for tests + */ +export const testMakeAReadAlong = async (page: Page) => { + await test.step("generate the readalong", async () => { + await page.getByTestId("ras-text-input").fill(testText); + + await page + .getByTestId("audio-btn-group") + .getByRole("button", { name: "File" }) + .click(); + await page.getByTestId("ras-audio-fileselector").click({ force: true }); + await page.getByTestId("ras-audio-fileselector").setInputFiles(testMp3Path); + await expect(async () => { + await expect( + page.getByTestId("next-step"), + "model is loaded", + ).not.toBeDisabled(); + + //create the readalong + await page.getByTestId("next-step").click(); + }).toPass(); + + //wait for edit page to load + await expect(async () => { + await expect(page.getByTestId("ra-header")).toBeVisible({ + timeout: 0, + }); + await expect(page.getByTestId("ra-header")).toBeEditable(); + //edit the headers + + await page + .getByTestId("ra-header") + .fill("Sentence Paragraph Page", { force: true }); + + await expect(page.getByTestId("ra-header")).toHaveValue( + "Sentence Paragraph Page", + ); + }).toPass(); + + await page + .getByTestId("ra-subheader") + .fill("by me", { force: true, timeout: 0 }); + await expect(page.getByTestId("ra-subheader")).toHaveValue("by me"); + //add translations + + await page + .locator("#t0b0d0p0s0") + .getByRole("button") + .click({ force: true, timeout: 0 }); + + await page + .locator("#t0b0d0p0s1") + .getByRole("button") + .click({ force: true, timeout: 0 }); + await page + .locator("#t0b0d0p1s0") + .getByRole("button") + .click({ force: true, timeout: 0 }); + //update translations + await expect(page.locator("#t0b0d0p0s0translation")).toBeEditable(); + await page + .locator("#t0b0d0p0s0translation") + .fill("Ceci est un test.", { force: true, timeout: 0 }); + await expect(page.locator("#t0b0d0p0s1translation")).toBeEditable(); + await page + .locator("#t0b0d0p0s1translation") + .fill("Phrase.", { force: true, timeout: 0 }); + await expect(page.locator("#t0b0d0p1s0translation")).toBeEditable(); + await page + .locator("#t0b0d0p1s0translation") + .fill("Paragraphe.", { force: true, timeout: 0 }); + + //upload a photo to page 1 + let fileChooserPromise = page.waitForEvent("filechooser"); + page.locator("#fileElem--t0b0d0").dispatchEvent("click"); + + let fileChooser = await fileChooserPromise; + fileChooser.setFiles(testAssetsPath + "page1.png"); + + await page.locator("#t0b0d0p0s0w0").dispatchEvent("click"); + //upload a photo to page 2 + fileChooserPromise = page.waitForEvent("filechooser"); + page.locator("#fileElem--t0b0d1").dispatchEvent("click"); + fileChooser = await fileChooserPromise; + fileChooser.setFiles(testAssetsPath + "page2.png"); + await expect(async () => { + await expect(page.locator("div.toast-message")).not.toBeVisible(); + }).toPass(); + }); +}; + +/* default before each */ +export const defaultBeforeEach = async (page: Page, browserName: string) => { + test.step("setup test", async () => { + test.skip( + browserName === "webkit", + "The aligner feature is not stable for webkit", + ); + + await page.goto("/", { waitUntil: "load" }); + await expect( + page.getByTestId("next-step"), + "Soundswallower model has loaded", + ).not.toBeDisabled(); + }); +}; + +/** + * @param text:string + * @return text:string + * Timestamps generated by alignment can be off by a couple of microseconds. + * The point of the test is check format not the values generated, + * therefore we zero the values to restrict the comparison to format checking + */ +export const replaceValuesWithZeroes = (text: string): string => { + return text.replace(/\d/g, "0"); +}; diff --git a/packages/web-component/src/components/read-along-component/read-along.tsx b/packages/web-component/src/components/read-along-component/read-along.tsx index 13139d8e..8aa1f0f0 100644 --- a/packages/web-component/src/components/read-along-component/read-along.tsx +++ b/packages/web-component/src/components/read-along-component/read-along.tsx @@ -1603,6 +1603,7 @@ export class ReadAlongComponent { attributes: NamedNodeMap; }): Element => ( <div + {...props.attributes} class={ "paragraph sentence__container theme--" + this.theme + @@ -1639,7 +1640,7 @@ export class ReadAlongComponent { } let nodeProps = {}; //attributes of sentence you want to retain - for (const attr of ["annotation-id", "do-not-align", "lang"]) { + for (const attr of ["annotation-id", "do-not-align", "lang", "id"]) { if (props.sentenceData.hasAttribute(attr)) { nodeProps[attr] = props.sentenceData.getAttribute(attr); }