From 15e45dad8012702ddbbc9235e0ba4852dbfdf100 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Fri, 15 Jul 2022 18:30:36 -0400 Subject: [PATCH 01/13] Drive Browser tests with `playwright` (#609) Replaces the [intern][]-powered functional test suite with one powered by [playwright][]. The majority of the changes made aim to preserve the structure and method names of the original `intern` suite. There are some out-of-the-box Playwright features that could replace many of our bespoke helpers, but that work should be done after the migration. As a result, the majority of the changes involve a combination of: * replacing `async "test ..."() { ... }` with `test("test ...", async ({ page }) => { ... }` * replacing calls in the style of `this.methodName` with calls in the style of `methodName(page)` In some cases, exceptions were made. For example, most calls to `this.clickSelector` were replaced with calls to [page.click][]. The unit test suite --- Playwright's focus on a browser-powered test harness make it an odd fit for our small suite of "unit" tests. Unlike their browser-test counterparts, the unit tests do not have a history of flakiness. This changeset defers migrating those tests off of `intern` for a later batch of work. [intern]: https://theintern.io [playwright]: https://playwright.dev [page.click]: https://playwright.dev/docs/api/class-page#page-click --- .github/workflows/ci.yml | 19 +- CONTRIBUTING.md | 52 +- intern.json | 1 - package.json | 11 +- playwright.config.ts | 27 + rollup.config.js | 22 - src/tests/fixtures/test.js | 20 +- src/tests/functional/async_script_tests.ts | 32 +- src/tests/functional/autofocus_tests.ts | 138 +- src/tests/functional/cache_observer_tests.ts | 30 +- src/tests/functional/drive_disabled_tests.ts | 80 +- src/tests/functional/drive_tests.ts | 49 +- src/tests/functional/form_submission_tests.ts | 1780 +++++++++-------- .../functional/frame_navigation_tests.ts | 37 +- src/tests/functional/frame_tests.ts | 1344 +++++++------ src/tests/functional/import_tests.ts | 21 +- src/tests/functional/index.ts | 18 - src/tests/functional/loading_tests.ts | 365 ++-- src/tests/functional/navigation_tests.ts | 681 ++++--- .../functional/pausable_rendering_tests.ts | 53 +- .../functional/pausable_requests_tests.ts | 57 +- src/tests/functional/preloader_tests.ts | 108 +- src/tests/functional/rendering_tests.ts | 648 +++--- .../functional/scroll_restoration_tests.ts | 54 +- src/tests/functional/stream_tests.ts | 95 +- src/tests/functional/visit_tests.ts | 275 ++- src/tests/helpers/functional_test_case.ts | 179 -- src/tests/helpers/page.ts | 241 +++ src/tests/helpers/remote_channel.ts | 36 - src/tests/helpers/turbo_drive_test_case.ts | 111 - src/tests/runner.js | 8 - tsconfig.json | 2 +- yarn.lock | 1479 ++++++++------ 33 files changed, 4125 insertions(+), 3948 deletions(-) create mode 100644 playwright.config.ts delete mode 100644 src/tests/functional/index.ts delete mode 100644 src/tests/helpers/functional_test_case.ts create mode 100644 src/tests/helpers/page.ts delete mode 100644 src/tests/helpers/remote_channel.ts delete mode 100644 src/tests/helpers/turbo_drive_test_case.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a307a8e68..649ffd8f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ jobs: runs-on: ubuntu-latest + steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 @@ -18,6 +19,7 @@ jobs: key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - run: yarn install + - run: yarn run playwright install --with-deps - run: yarn build - name: Set Chrome Version @@ -29,8 +31,21 @@ jobs: - name: Lint run: yarn lint - - name: Test - run: yarn test + - name: Unit Test + run: yarn test:unit + + - name: Chrome Test + run: yarn test:browser --project=chrome + + - name: Firefox Test + run: yarn test:browser --project=firefox + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v2 + with: + name: playwright-report + path: playwright-report - name: Publish dev build run: .github/scripts/publish-dev-build '${{ secrets.DEV_BUILD_GITHUB_TOKEN }}' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c4e7170a1..5106f6a85 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,21 +34,46 @@ Once you are done developing the feature or bug fix you have 2 options: 2. Run a local webserver and checkout your changes manually ### Testing -The library is tested by running the test suite (found in: `src/tests/*`) against headless browsers. The browsers are setup in `intern.json` check it out to see the used browser environments. +The library is tested by running the test suite (found in: `src/tests/*`) against headless browsers. The browsers are setup in [intern.json](./intern.json) and [playwright.config.ts](./playwright.config.ts). Check them out to see the used browser environments. To override the ChromeDriver version, declare the `CHROMEVER` environment variable. +First, install the drivers to test the suite in browsers: + +``bash +yarn playwright install --with-deps +``` + The tests are using the compiled version of the library and they are themselves also compiled. To compile the tests and library and watch for changes: ```bash yarn watch ``` -To run the tests: +To run the unit tests: + +```bash +yarn test:unit +``` + +To run the browser tests: + +```bash +yarn test:browser +``` + +To run the browser suite against a particular browser (one of +`chrome|firefox`), pass the value as the `--project=$BROWSER` flag: ```bash -yarn test +yarn test:browser --project=chrome +``` + +To run the browser tests in a "headed" browser, pass the `--headed` flag: + +```bash +yarn test:browser --project=chrome --headed ``` ### Test files @@ -58,14 +83,23 @@ The html files needed for the tests are stored in: `src/tests/fixtures/` ### Run single test -To focus on single test grep for it: -```javascript -yarn test --grep TEST_CASE_NAME +To focus on single test, pass its file path: + +```bas +yarn test:browser TEST_FILE ``` -Where the `TEST_CASE_NAME` is the name of test you want to run. For example: -```javascript -yarn test --grep 'triggers before-render and render events' +Where the `TEST_FILE` is the name of test you want to run. For example: + +```base +yarn test:browser src/tests/functional/drive_tests.ts +``` + +To execute a particular test, append `:LINE` where `LINE` is the line number of +the call to `test("...")`: + +```bash +yarn test:browser src/tests/functional/drive_tests.ts:11 ``` ### Local webserver diff --git a/intern.json b/intern.json index 60c07e97e..583314227 100644 --- a/intern.json +++ b/intern.json @@ -1,6 +1,5 @@ { "suites": "dist/tests/unit.js", - "functionalSuites": "dist/tests/functional.js", "environments": [ { "browserName": "chrome", diff --git a/package.json b/package.json index 1ae29835a..5a74fcfa5 100644 --- a/package.json +++ b/package.json @@ -35,12 +35,14 @@ "access": "public" }, "devDependencies": { + "@playwright/test": "^1.22.2", "@rollup/plugin-node-resolve": "13.1.3", "@rollup/plugin-typescript": "8.3.1", "@types/multer": "^1.4.5", "@typescript-eslint/eslint-plugin": "^5.20.0", "@typescript-eslint/parser": "^5.20.0", "arg": "^5.0.1", + "chai": "~4.3.4", "eslint": "^8.13.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.0.0", @@ -58,10 +60,15 @@ "build:win": "tsc --noEmit false --declaration true --emitDeclarationOnly true --outDir dist/types & rollup -c", "watch": "rollup -wc", "start": "node src/tests/runner.js serveOnly", - "test": "NODE_OPTIONS=--inspect node src/tests/runner.js", - "test:win": "SET NODE_OPTIONS=--inspect & node src/tests/runner.js", + "test": "yarn test:unit && yarn test:browser", + "test:browser": "playwright test", + "test:unit": "NODE_OPTIONS=--inspect node src/tests/runner.js", + "test:unit:win": "SET NODE_OPTIONS=--inspect & node src/tests/runner.js", "prerelease": "yarn build && git --no-pager diff && echo && npm pack --dry-run && echo && read -n 1 -p \"Look OK? Press any key to publish and commit v$npm_package_version\" && echo", "release": "npm publish && git commit -am \"$npm_package_name v$npm_package_version\" && git push", "lint": "eslint . --ext .ts" + }, + "engines": { + "node": ">= 14" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..2e8d2f27a --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,27 @@ +import { type PlaywrightTestConfig, devices } from "@playwright/test" + +const config: PlaywrightTestConfig = { + projects: [ + { + name: "chrome", + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + ], + testDir: "./src/tests/functional", + testMatch: /.*_tests\.ts/, + webServer: { + command: "yarn start", + url: "http://localhost:9000/src/tests/fixtures/test.js", + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI, + }, + use: { + baseURL: "http://localhost:9000/", + }, +} + +export default config diff --git a/rollup.config.js b/rollup.config.js index f5fa09df3..bca5ee055 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -30,28 +30,6 @@ export default [ } }, - { - input: "src/tests/functional/index.ts", - output: [ - { - file: "dist/tests/functional.js", - format: "cjs", - sourcemap: true - } - ], - plugins: [ - resolve(), - typescript() - ], - external: [ - "http", - "intern" - ], - watch: { - include: "src/tests/**" - } - }, - { input: "src/tests/unit/index.ts", output: [ diff --git a/src/tests/fixtures/test.js b/src/tests/fixtures/test.js index 369c0098c..fa98eb3d2 100644 --- a/src/tests/fixtures/test.js +++ b/src/tests/fixtures/test.js @@ -1,4 +1,22 @@ (function(eventNames) { + function serializeToChannel(object, returned = {}) { + for (const key in object) { + const value = object[key] + + if (value instanceof URL) { + returned[key] = value.toJSON() + } else if (value instanceof Element) { + returned[key] = value.outerHTML + } else if (typeof value == "object") { + returned[key] = serializeToChannel(value) + } else { + returned[key] = value + } + } + + return returned + } + window.eventLogs = [] for (var i = 0; i < eventNames.length; i++) { @@ -9,7 +27,7 @@ function eventListener(event) { const skipped = document.documentElement.getAttribute("data-skip-event-details") || "" - eventLogs.push([event.type, skipped.includes(event.type) ? {} : event.detail, event.target.id]) + eventLogs.push([event.type, serializeToChannel(skipped.includes(event.type) ? {} : event.detail), event.target.id]) } window.mutationLogs = [] diff --git a/src/tests/functional/async_script_tests.ts b/src/tests/functional/async_script_tests.ts index 025127c89..f96a6852a 100644 --- a/src/tests/functional/async_script_tests.ts +++ b/src/tests/functional/async_script_tests.ts @@ -1,20 +1,20 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" +import { readEventLogs, visitAction } from "../helpers/page" -export class AsyncScriptTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/async_script.html") - } +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/async_script.html") + await readEventLogs(page) +}) - async "test does not emit turbo:load when loaded asynchronously after DOMContentLoaded"() { - const events = await this.eventLogChannel.read() - this.assert.deepEqual(events, []) - } +test("test does not emit turbo:load when loaded asynchronously after DOMContentLoaded", async ({ page }) => { + const events = await readEventLogs(page) - async "test following a link when loaded asynchronously after DOMContentLoaded"() { - this.clickSelector("#async-link") - await this.nextBody - this.assert.equal(await this.visitAction, "advance") - } -} + assert.deepEqual(events, []) +}) -AsyncScriptTests.registerSuite() +test("test following a link when loaded asynchronously after DOMContentLoaded", async ({ page }) => { + await page.click("#async-link") + + assert.equal(await visitAction(page), "advance") +}) diff --git a/src/tests/functional/autofocus_tests.ts b/src/tests/functional/autofocus_tests.ts index 79c349386..03deea684 100644 --- a/src/tests/functional/autofocus_tests.ts +++ b/src/tests/functional/autofocus_tests.ts @@ -1,78 +1,80 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" +import { hasSelector, nextBeat } from "../helpers/page" -export class AutofocusTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/autofocus.html") - } +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/autofocus.html") +}) - async "test autofocus first autofocus element on load"() { - await this.nextBeat - this.assert.ok( - await this.hasSelector("#first-autofocus-element:focus"), - "focuses the first [autofocus] element on the page" - ) - this.assert.notOk( - await this.hasSelector("#second-autofocus-element:focus"), - "focuses the first [autofocus] element on the page" - ) - } +test("test autofocus first autofocus element on load", async ({ page }) => { + await nextBeat() + assert.ok( + await hasSelector(page, "#first-autofocus-element:focus"), + "focuses the first [autofocus] element on the page" + ) + assert.notOk( + await hasSelector(page, "#second-autofocus-element:focus"), + "focuses the first [autofocus] element on the page" + ) +}) - async "test autofocus first [autofocus] element on visit"() { - await this.goToLocation("/src/tests/fixtures/navigation.html") - await this.clickSelector("#autofocus-link") - await this.sleep(500) +test("test autofocus first [autofocus] element on visit", async ({ page }) => { + await page.goto("/src/tests/fixtures/navigation.html") + await page.click("#autofocus-link") + await nextBeat() - this.assert.ok( - await this.hasSelector("#first-autofocus-element:focus"), - "focuses the first [autofocus] element on the page" - ) - this.assert.notOk( - await this.hasSelector("#second-autofocus-element:focus"), - "focuses the first [autofocus] element on the page" - ) - } + assert.ok( + await hasSelector(page, "#first-autofocus-element:focus"), + "focuses the first [autofocus] element on the page" + ) + assert.notOk( + await hasSelector(page, "#second-autofocus-element:focus"), + "focuses the first [autofocus] element on the page" + ) +}) - async "test navigating a frame with a descendant link autofocuses [autofocus]:first-of-type"() { - await this.clickSelector("#frame-inner-link") - await this.nextBeat +test("test navigating a frame with a descendant link autofocuses [autofocus]:first-of-type", async ({ page }) => { + await page.click("#frame-inner-link") + await nextBeat() - this.assert.ok( - await this.hasSelector("#frames-form-first-autofocus-element:focus"), - "focuses the first [autofocus] element in frame" - ) - this.assert.notOk( - await this.hasSelector("#frames-form-second-autofocus-element:focus"), - "focuses the first [autofocus] element in frame" - ) - } + assert.ok( + await hasSelector(page, "#frames-form-first-autofocus-element:focus"), + "focuses the first [autofocus] element in frame" + ) + assert.notOk( + await hasSelector(page, "#frames-form-second-autofocus-element:focus"), + "focuses the first [autofocus] element in frame" + ) +}) - async "test navigating a frame with a link targeting the frame autofocuses [autofocus]:first-of-type"() { - await this.clickSelector("#frame-outer-link") - await this.nextBeat +test("test navigating a frame with a link targeting the frame autofocuses [autofocus]:first-of-type", async ({ + page, +}) => { + await page.click("#frame-outer-link") + await nextBeat() - this.assert.ok( - await this.hasSelector("#frames-form-first-autofocus-element:focus"), - "focuses the first [autofocus] element in frame" - ) - this.assert.notOk( - await this.hasSelector("#frames-form-second-autofocus-element:focus"), - "focuses the first [autofocus] element in frame" - ) - } + assert.ok( + await hasSelector(page, "#frames-form-first-autofocus-element:focus"), + "focuses the first [autofocus] element in frame" + ) + assert.notOk( + await hasSelector(page, "#frames-form-second-autofocus-element:focus"), + "focuses the first [autofocus] element in frame" + ) +}) - async "test navigating a frame with a turbo-frame targeting the frame autofocuses [autofocus]:first-of-type"() { - await this.clickSelector("#drives-frame-target-link") - await this.nextBeat +test("test navigating a frame with a turbo-frame targeting the frame autofocuses [autofocus]:first-of-type", async ({ + page, +}) => { + await page.click("#drives-frame-target-link") + await nextBeat() - this.assert.ok( - await this.hasSelector("#frames-form-first-autofocus-element:focus"), - "focuses the first [autofocus] element in frame" - ) - this.assert.notOk( - await this.hasSelector("#frames-form-second-autofocus-element:focus"), - "focuses the first [autofocus] element in frame" - ) - } -} - -AutofocusTests.registerSuite() + assert.ok( + await hasSelector(page, "#frames-form-first-autofocus-element:focus"), + "focuses the first [autofocus] element in frame" + ) + assert.notOk( + await hasSelector(page, "#frames-form-second-autofocus-element:focus"), + "focuses the first [autofocus] element in frame" + ) +}) diff --git a/src/tests/functional/cache_observer_tests.ts b/src/tests/functional/cache_observer_tests.ts index f3797d57e..edc923391 100644 --- a/src/tests/functional/cache_observer_tests.ts +++ b/src/tests/functional/cache_observer_tests.ts @@ -1,18 +1,16 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" +import { hasSelector, nextBody } from "../helpers/page" -export class CacheObserverTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/cache_observer.html") - } +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/cache_observer.html") +}) - async "test removes stale elements"() { - this.assert(await this.hasSelector("#flash")) - this.clickSelector("#link") - await this.nextBody - await this.goBack() - await this.nextBody - this.assert.notOk(await this.hasSelector("#flash")) - } -} - -CacheObserverTests.registerSuite() +test("test removes stale elements", async ({ page }) => { + assert(await hasSelector(page, "#flash")) + page.click("#link") + await nextBody(page) + await page.goBack() + await nextBody(page) + assert.notOk(await hasSelector(page, "#flash")) +}) diff --git a/src/tests/functional/drive_disabled_tests.ts b/src/tests/functional/drive_disabled_tests.ts index 42cf78c4f..b8a422636 100644 --- a/src/tests/functional/drive_disabled_tests.ts +++ b/src/tests/functional/drive_disabled_tests.ts @@ -1,36 +1,44 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" - -export class DriveDisabledTests extends TurboDriveTestCase { - path = "/src/tests/fixtures/drive_disabled.html" - - async setup() { - await this.goToLocation(this.path) - } - - async "test drive disabled by default; click normal link"() { - this.clickSelector("#drive_disabled") - await this.nextBody - this.assert.equal(await this.pathname, this.path) - this.assert.equal(await this.visitAction, "load") - } - - async "test drive disabled by default; click link inside data-turbo='true'"() { - this.clickSelector("#drive_enabled") - await this.nextBody - this.assert.equal(await this.pathname, this.path) - this.assert.equal(await this.visitAction, "advance") - } - - async "test drive disabled by default; submit form inside data-turbo='true'"() { - await this.setLocalStorageFromEvent("turbo:submit-start", "formSubmitted", "true") - - this.clickSelector("#no_submitter_drive_enabled a#requestSubmit") - await this.nextBody - this.assert.ok(await this.getFromLocalStorage("formSubmitted")) - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.visitAction, "advance") - this.assert.equal(await this.getSearchParam("greeting"), "Hello from a redirect") - } -} - -DriveDisabledTests.registerSuite() +import { test } from "@playwright/test" +import { assert } from "chai" +import { + getFromLocalStorage, + nextBody, + pathname, + searchParams, + setLocalStorageFromEvent, + visitAction, +} from "../helpers/page" + +const path = "/src/tests/fixtures/drive_disabled.html" + +test.beforeEach(async ({ page }) => { + await page.goto(path) +}) + +test("test drive disabled by default; click normal link", async ({ page }) => { + await page.click("#drive_disabled") + await nextBody(page) + + assert.equal(pathname(page.url()), path) + assert.equal(await visitAction(page), "load") +}) + +test("test drive disabled by default; click link inside data-turbo='true'", async ({ page }) => { + await page.click("#drive_enabled") + await nextBody(page) + + assert.equal(pathname(page.url()), path) + assert.equal(await visitAction(page), "advance") +}) + +test("test drive disabled by default; submit form inside data-turbo='true'", async ({ page }) => { + await setLocalStorageFromEvent(page, "turbo:submit-start", "formSubmitted", "true") + + await page.click("#no_submitter_drive_enabled a#requestSubmit") + await nextBody(page) + + assert.ok(await getFromLocalStorage(page, "formSubmitted")) + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(await visitAction(page), "advance") + assert.equal(await searchParams(page.url()).get("greeting"), "Hello from a redirect") +}) diff --git a/src/tests/functional/drive_tests.ts b/src/tests/functional/drive_tests.ts index bc660d302..26d840f59 100644 --- a/src/tests/functional/drive_tests.ts +++ b/src/tests/functional/drive_tests.ts @@ -1,31 +1,28 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" +import { nextBody, pathname, visitAction } from "../helpers/page" -export class DriveTests extends TurboDriveTestCase { - path = "/src/tests/fixtures/drive.html" +const path = "/src/tests/fixtures/drive.html" - async setup() { - await this.goToLocation(this.path) - } +test.beforeEach(async ({ page }) => { + await page.goto(path) +}) - async "test drive enabled by default; click normal link"() { - this.clickSelector("#drive_enabled") - await this.nextBody - this.assert.equal(await this.pathname, this.path) - this.assert.equal(await this.visitAction, "advance") - } +test("test drive enabled by default; click normal link", async ({ page }) => { + page.click("#drive_enabled") + await nextBody(page) + assert.equal(pathname(page.url()), path) +}) - async "test drive to external link"() { - this.clickSelector("#drive_enabled_external") - await this.nextBody - this.assert.equal(await this.remote.execute(() => window.location.href), "https://example.com/") - } +test("test drive to external link", async ({ page }) => { + page.click("#drive_enabled_external") + await nextBody(page) + assert.equal(await page.evaluate(() => window.location.href), "https://example.com/") +}) - async "test drive enabled by default; click link inside data-turbo='false'"() { - this.clickSelector("#drive_disabled") - await this.nextBody - this.assert.equal(await this.pathname, this.path) - this.assert.equal(await this.visitAction, "load") - } -} - -DriveTests.registerSuite() +test("test drive enabled by default; click link inside data-turbo='false'", async ({ page }) => { + page.click("#drive_disabled") + await nextBody(page) + assert.equal(pathname(page.url()), path) + assert.equal(await visitAction(page), "load") +}) diff --git a/src/tests/functional/form_submission_tests.ts b/src/tests/functional/form_submission_tests.ts index bda7f0494..cc713df85 100644 --- a/src/tests/functional/form_submission_tests.ts +++ b/src/tests/functional/form_submission_tests.ts @@ -1,965 +1,985 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" - -export class FormSubmissionTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/form.html") - await this.setLocalStorageFromEvent("turbo:submit-start", "formSubmitStarted", "true") - await this.setLocalStorageFromEvent("turbo:submit-end", "formSubmitEnded", "true") - } - - async "test standard form submission renders a progress bar"() { - await this.remote.execute(() => window.Turbo.setProgressBarDelay(0)) - await this.clickSelector("#standard form.sleep input[type=submit]") - - await this.waitUntilSelector(".turbo-progress-bar") - this.assert.ok(await this.hasSelector(".turbo-progress-bar"), "displays progress bar") - - await this.nextBody - await this.waitUntilNoSelector(".turbo-progress-bar") - - this.assert.notOk(await this.hasSelector(".turbo-progress-bar"), "hides progress bar") - } - - async "test form submission with confirmation confirmed"() { - await this.clickSelector("#standard form.confirm input[type=submit]") - - this.assert.equal(await this.getAlertText(), "Are you sure?") - await this.acceptAlert() - await this.nextEventNamed("turbo:load") - this.assert.ok(await this.formSubmitStarted) - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - } - - async "test form submission with confirmation cancelled"() { - await this.clickSelector("#standard form.confirm input[type=submit]") - - this.assert.equal(await this.getAlertText(), "Are you sure?") - await this.dismissAlert() - this.assert.notOk(await this.formSubmitStarted) - } - - async "test form submission with secondary submitter click - confirmation confirmed"() { - await this.clickSelector("#standard form.confirm #secondary_submitter") - - this.assert.equal(await this.getAlertText(), "Are you really sure?") - await this.acceptAlert() - await this.nextEventNamed("turbo:load") - this.assert.ok(await this.formSubmitStarted) - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "advance") - this.assert.equal(await this.getSearchParam("greeting"), "secondary_submitter") - } - - async "test form submission with secondary submitter click - confirmation cancelled"() { - await this.clickSelector("#standard form.confirm #secondary_submitter") - - this.assert.equal(await this.getAlertText(), "Are you really sure?") - await this.dismissAlert() - this.assert.notOk(await this.formSubmitStarted) - } - - async "test from submission with confirmation overriden"() { - await this.remote.execute(() => window.Turbo.setConfirmMethod(() => Promise.resolve(confirm("Overriden message")))) - - await this.clickSelector("#standard form.confirm input[type=submit]") - - this.assert.equal(await this.getAlertText(), "Overriden message") - await this.acceptAlert() - this.assert.ok(await this.formSubmitStarted) - } - - async "test standard form submission does not render a progress bar before expiring the delay"() { - await this.remote.execute(() => window.Turbo.setProgressBarDelay(500)) - await this.clickSelector("#standard form.redirect input[type=submit]") +import { Page, test } from "@playwright/test" +import { assert } from "chai" +import { + getFromLocalStorage, + getSearchParam, + hasSelector, + isScrolledToTop, + nextAttributeMutationNamed, + nextBeat, + nextBody, + nextEventNamed, + nextEventOnTarget, + noNextEventNamed, + outerHTMLForSelector, + pathname, + readEventLogs, + scrollToSelector, + search, + searchParams, + setLocalStorageFromEvent, + visitAction, + waitUntilSelector, + waitUntilNoSelector, +} from "../helpers/page" + +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/form.html") + await setLocalStorageFromEvent(page, "turbo:submit-start", "formSubmitStarted", "true") + await setLocalStorageFromEvent(page, "turbo:submit-end", "formSubmitEnded", "true") + await readEventLogs(page) +}) + +test("test standard form submission renders a progress bar", async ({ page }) => { + await page.evaluate(() => window.Turbo.setProgressBarDelay(0)) + await page.click("#standard form.sleep input[type=submit]") + + await waitUntilSelector(page, ".turbo-progress-bar") + assert.ok(await hasSelector(page, ".turbo-progress-bar"), "displays progress bar") + + await nextBody(page) + await waitUntilNoSelector(page, ".turbo-progress-bar") + + assert.notOk(await hasSelector(page, ".turbo-progress-bar"), "hides progress bar") +}) + +test("test form submission with confirmation confirmed", async ({ page }) => { + page.on("dialog", (alert) => { + assert.equal(alert.message(), "Are you sure?") + alert.accept() + }) + + await page.click("#standard form.confirm input[type=submit]") + + await nextEventNamed(page, "turbo:load") + assert.ok(await formSubmitStarted(page)) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") +}) + +test("test form submission with confirmation cancelled", async ({ page }) => { + page.on("dialog", (alert) => { + assert.equal(alert.message(), "Are you sure?") + alert.dismiss() + }) + await page.click("#standard form.confirm input[type=submit]") + + assert.notOk(await formSubmitStarted(page)) +}) + +test("test form submission with secondary submitter click - confirmation confirmed", async ({ page }) => { + page.on("dialog", (alert) => { + assert.equal(alert.message(), "Are you really sure?") + alert.accept() + }) + + await page.click("#standard form.confirm #secondary_submitter") + + await nextEventNamed(page, "turbo:load") + assert.ok(await formSubmitStarted(page)) + assert.equal(await pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "advance") + assert.equal(getSearchParam(page.url(), "greeting"), "secondary_submitter") +}) + +test("test form submission with secondary submitter click - confirmation cancelled", async ({ page }) => { + page.on("dialog", (alert) => { + assert.equal(alert.message(), "Are you really sure?") + alert.dismiss() + }) + + await page.click("#standard form.confirm #secondary_submitter") + + assert.notOk(await formSubmitStarted(page)) +}) + +test("test from submission with confirmation overriden", async ({ page }) => { + page.on("dialog", (alert) => { + assert.equal(alert.message(), "Overriden message") + alert.accept() + }) + + await page.evaluate(() => window.Turbo.setConfirmMethod(() => Promise.resolve(confirm("Overriden message")))) + await page.click("#standard form.confirm input[type=submit]") + + assert.ok(await formSubmitStarted(page)) +}) + +test("test standard form submission does not render a progress bar before expiring the delay", async ({ page }) => { + await page.evaluate(() => window.Turbo.setProgressBarDelay(500)) + await page.click("#standard form.redirect input[type=submit]") + + assert.notOk(await hasSelector(page, ".turbo-progress-bar"), "does not show progress bar before delay") +}) + +test("test standard form submission with redirect response", async ({ page }) => { + await page.click("#standard form.redirect input[type=submit]") + await nextBody(page) + + assert.ok(await formSubmitStarted(page)) + assert.equal(await pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(await visitAction(page), "advance") + assert.equal(getSearchParam(page.url(), "greeting"), "Hello from a redirect") + assert.equal( + await nextAttributeMutationNamed(page, "html", "aria-busy"), + "true", + "sets [aria-busy] on the document element" + ) + assert.equal( + await nextAttributeMutationNamed(page, "html", "aria-busy"), + null, + "removes [aria-busy] from the document element" + ) +}) + +test("test standard POST form submission events", async ({ page }) => { + await page.click("#standard-post-form-submit") + + assert.ok(await formSubmitStarted(page), "fires turbo:submit-start") + + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") + + assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) + + await nextEventNamed(page, "turbo:before-fetch-response") + + assert.ok(await formSubmitEnded(page), "fires turbo:submit-end") + + await nextEventNamed(page, "turbo:before-visit") + await nextEventNamed(page, "turbo:visit") + await nextEventNamed(page, "turbo:before-cache") + await nextEventNamed(page, "turbo:before-render") + await nextEventNamed(page, "turbo:render") + await nextEventNamed(page, "turbo:load") +}) + +test("test standard POST form submission merges values from both searchParams and body", async ({ page }) => { + await page.click("#form-action-post-redirect-self-q-b") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(getSearchParam(page.url(), "q"), "b") + assert.equal(getSearchParam(page.url(), "sort"), "asc") +}) + +test("test standard POST form submission toggles submitter [disabled] attribute", async ({ page }) => { + await page.click("#standard-post-form-submit") + + assert.equal( + await nextAttributeMutationNamed(page, "standard-post-form-submit", "disabled"), + "", + "sets [disabled] on the submitter" + ) + assert.equal( + await nextAttributeMutationNamed(page, "standard-post-form-submit", "disabled"), + null, + "removes [disabled] from the submitter" + ) +}) + +test("test standard GET form submission", async ({ page }) => { + await page.click("#standard form.greeting input[type=submit]") + await nextBody(page) + + assert.ok(await formSubmitStarted(page)) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "advance") + assert.equal(getSearchParam(page.url(), "greeting"), "Hello from a form") +}) + +test("test standard GET form submission with data-turbo-stream", async ({ page }) => { + await page.click("#standard-get-form-with-stream-opt-in-submit") + + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") + + assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) +}) + +test("test standard GET form submission events", async ({ page }) => { + await page.click("#standard-get-form-submit") + + assert.ok(await formSubmitStarted(page), "fires turbo:submit-start") + + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") + + assert.notOk(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) + + await nextEventNamed(page, "turbo:before-fetch-response") + + assert.ok(await formSubmitEnded(page), "fires turbo:submit-end") + + await nextEventNamed(page, "turbo:before-visit") + await nextEventNamed(page, "turbo:visit") + await nextEventNamed(page, "turbo:before-cache") + await nextEventNamed(page, "turbo:before-render") + await nextEventNamed(page, "turbo:render") + await nextEventNamed(page, "turbo:load") +}) + +test("test standard GET form submission does not incorporate the current page's URLSearchParams values into the submission", async ({ + page, +}) => { + await page.click("#form-action-self-sort") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(search(page.url()), "?sort=asc") + + await page.click("#form-action-none-q-a") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(search(page.url()), "?q=a", "navigates without omitted keys") +}) + +test("test standard GET form submission does not merge values into the [action] attribute", async ({ page }) => { + await page.click("#form-action-self-sort") + await nextBody(page) + + assert.equal(await pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(await search(page.url()), "?sort=asc") + + await page.click("#form-action-self-q-b") + await nextBody(page) + + assert.equal(await pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(await search(page.url()), "?q=b", "navigates without omitted keys") +}) + +test("test standard GET form submission omits the [action] value's URLSearchParams from the submission", async ({ + page, +}) => { + await page.click("#form-action-self-submit") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(search(page.url()), "") +}) + +test("test standard GET form submission toggles submitter [disabled] attribute", async ({ page }) => { + await page.click("#standard-get-form-submit") + + assert.equal( + await nextAttributeMutationNamed(page, "standard-get-form-submit", "disabled"), + "", + "sets [disabled] on the submitter" + ) + assert.equal( + await nextAttributeMutationNamed(page, "standard-get-form-submit", "disabled"), + null, + "removes [disabled] from the submitter" + ) +}) + +test("test standard GET form submission appending keys", async ({ page }) => { + await page.goto("/src/tests/fixtures/form.html?query=1") + await page.click("#standard form.conflicting-values input[type=submit]") + await nextBody(page) - this.assert.notOk(await this.hasSelector(".turbo-progress-bar"), "does not show progress bar before delay") - } - - async "test standard form submission with redirect response"() { - await this.clickSelector("#standard form.redirect input[type=submit]") - await this.nextBody - - this.assert.ok(await this.formSubmitStarted) - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.visitAction, "advance") - this.assert.equal(await this.getSearchParam("greeting"), "Hello from a redirect") - this.assert.equal( - await this.nextAttributeMutationNamed("html", "aria-busy"), - "true", - "sets [aria-busy] on the document element" - ) - this.assert.equal( - await this.nextAttributeMutationNamed("html", "aria-busy"), - null, - "removes [aria-busy] from the document element" - ) - } - - async "test standard POST form submission events"() { - await this.clickSelector("#standard-post-form-submit") - - this.assert.ok(await this.formSubmitStarted, "fires turbo:submit-start") + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(getSearchParam(page.url(), "query"), "2") +}) - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") - - this.assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) +test("test standard form submission with empty created response", async ({ page }) => { + const htmlBefore = await outerHTMLForSelector(page, "body") + const button = await page.locator("#standard form.created input[type=submit]") + await button.click() + await nextBeat() - await this.nextEventNamed("turbo:before-fetch-response") + const htmlAfter = await outerHTMLForSelector(page, "body") + assert.equal(htmlAfter, htmlBefore) +}) - this.assert.ok(await this.formSubmitEnded, "fires turbo:submit-end") +test("test standard form submission with empty no-content response", async ({ page }) => { + const htmlBefore = await outerHTMLForSelector(page, "body") + const button = await page.locator("#standard form.no-content input[type=submit]") + await button.click() + await nextBeat() + + const htmlAfter = await outerHTMLForSelector(page, "body") + assert.equal(htmlAfter, htmlBefore) +}) + +test("test standard POST form submission with multipart/form-data enctype", async ({ page }) => { + await page.click("#standard form[method=post][enctype] input[type=submit]") + await nextBeat() + + const enctype = getSearchParam(page.url(), "enctype") + assert.ok(enctype?.startsWith("multipart/form-data"), "submits a multipart/form-data request") +}) + +test("test standard GET form submission ignores enctype", async ({ page }) => { + await page.click("#standard form[method=get][enctype] input[type=submit]") + await nextBeat() + + const enctype = getSearchParam(page.url(), "enctype") + assert.notOk(enctype, "GET form submissions ignore enctype") +}) + +test("test standard POST form submission without an enctype", async ({ page }) => { + await page.click("#standard form[method=post].no-enctype input[type=submit]") + await nextBeat() + + const enctype = getSearchParam(page.url(), "enctype") + assert.ok( + enctype?.startsWith("application/x-www-form-urlencoded"), + "submits a application/x-www-form-urlencoded request" + ) +}) + +test("test no-action form submission with single parameter", async ({ page }) => { + await page.click("#no-action form.single input[type=submit]") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(getSearchParam(page.url(), "query"), "1") + + await page.click("#no-action form.single input[type=submit]") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(getSearchParam(page.url(), "query"), "1") + + await page.goto("/src/tests/fixtures/form.html?query=2") + await page.click("#no-action form.single input[type=submit]") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(getSearchParam(page.url(), "query"), "1") +}) + +test("test no-action form submission with multiple parameters", async ({ page }) => { + await page.goto("/src/tests/fixtures/form.html?query=2") + await page.click("#no-action form.multiple input[type=submit]") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.deepEqual(searchParams(page.url()).getAll("query"), ["1", "2"]) + + await page.click("#no-action form.multiple input[type=submit]") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.deepEqual(searchParams(page.url()).getAll("query"), ["1", "2"]) +}) + +test("test no-action form submission submitter parameters", async ({ page }) => { + await page.click("#no-action form.button-param [type=submit]") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(getSearchParam(page.url(), "query"), "1") + assert.deepEqual(searchParams(page.url()).getAll("button"), []) + + await page.click("#no-action form.button-param [type=submit]") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(getSearchParam(page.url(), "query"), "1") + assert.deepEqual(searchParams(page.url()).getAll("button"), []) +}) + +test("test submitter with blank formaction submits to the current page", async ({ page }) => { + await page.click("#blank-formaction button") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.ok(await hasSelector(page, "#blank-formaction"), "overrides form[action] navigation") +}) + +test("test input named action with no action attribute", async ({ page }) => { + await page.click("#action-input form.no-action [type=submit]") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(getSearchParam(page.url(), "action"), "1") + assert.equal(getSearchParam(page.url(), "query"), "1") +}) + +test("test input named action with action attribute", async ({ page }) => { + await page.click("#action-input form.action [type=submit]") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(getSearchParam(page.url(), "action"), "1") + assert.equal(getSearchParam(page.url(), "query"), "1") +}) + +test("test invalid form submission with unprocessable entity status", async ({ page }) => { + await page.click("#reject form.unprocessable_entity input[type=submit]") + await nextBody(page) - await this.nextEventNamed("turbo:before-visit") - await this.nextEventNamed("turbo:visit") - await this.nextEventNamed("turbo:before-cache") - await this.nextEventNamed("turbo:before-render") - await this.nextEventNamed("turbo:render") - await this.nextEventNamed("turbo:load") - } + const title = await page.locator("h1") + assert.equal(await title.textContent(), "Unprocessable Entity", "renders the response HTML") + assert.notOk(await hasSelector(page, "#frame form.reject"), "replaces entire page") +}) - async "test standard POST form submission merges values from both searchParams and body"() { - await this.clickSelector("#form-action-post-redirect-self-q-b") - await this.nextBody +test("test invalid form submission with long form", async ({ page }) => { + await scrollToSelector(page, "#reject form.unprocessable_entity_with_tall_form input[type=submit]") + await page.click("#reject form.unprocessable_entity_with_tall_form input[type=submit]") + await nextBody(page) - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.getSearchParam("q"), "b") - this.assert.equal(await this.getSearchParam("sort"), "asc") - } - - async "test standard POST form submission toggles submitter [disabled] attribute"() { - await this.clickSelector("#standard-post-form-submit") - - this.assert.equal( - await this.nextAttributeMutationNamed("standard-post-form-submit", "disabled"), - "", - "sets [disabled] on the submitter" - ) - this.assert.equal( - await this.nextAttributeMutationNamed("standard-post-form-submit", "disabled"), - null, - "removes [disabled] from the submitter" - ) - } - - async "test standard GET form submission"() { - await this.clickSelector("#standard form.greeting input[type=submit]") - await this.nextBody - - this.assert.ok(await this.formSubmitStarted) - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "advance") - this.assert.equal(await this.getSearchParam("greeting"), "Hello from a form") - } - - async "test standard GET form submission with data-turbo-stream"() { - await this.clickSelector("#standard-get-form-with-stream-opt-in-submit") - - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") - - this.assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) - } - - async "test standard GET form submission events"() { - await this.clickSelector("#standard-get-form-submit") - - this.assert.ok(await this.formSubmitStarted, "fires turbo:submit-start") - - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") - - this.assert.notOk(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) - - await this.nextEventNamed("turbo:before-fetch-response") - - this.assert.ok(await this.formSubmitEnded, "fires turbo:submit-end") - - await this.nextEventNamed("turbo:before-visit") - await this.nextEventNamed("turbo:visit") - await this.nextEventNamed("turbo:before-cache") - await this.nextEventNamed("turbo:before-render") - await this.nextEventNamed("turbo:render") - await this.nextEventNamed("turbo:load") - } - - async "test standard GET form submission does not incorporate the current page's URLSearchParams values into the submission"() { - await this.clickSelector("#form-action-self-sort") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.search, "?sort=asc") - - await this.clickSelector("#form-action-none-q-a") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.search, "?q=a", "navigates without omitted keys") - } + const title = await page.locator("h1") + assert.equal(await title.textContent(), "Unprocessable Entity", "renders the response HTML") + assert(await isScrolledToTop(page), "page is scrolled to the top") + assert.notOk(await hasSelector(page, "#frame form.reject"), "replaces entire page") +}) + +test("test invalid form submission with server error status", async ({ page }) => { + assert(await hasSelector(page, "head > #form-fixture-styles")) + await page.click("#reject form.internal_server_error input[type=submit]") + await nextBody(page) + + const title = await page.locator("h1") + assert.equal(await title.textContent(), "Internal Server Error", "renders the response HTML") + assert.notOk(await hasSelector(page, "head > #form-fixture-styles"), "replaces head") + assert.notOk(await hasSelector(page, "#frame form.reject"), "replaces entire page") +}) - async "test standard GET form submission does not merge values into the [action] attribute"() { - await this.clickSelector("#form-action-self-sort") - await this.nextBody +test("test submitter form submission reads button attributes", async ({ page }) => { + const button = await page.locator("#submitter form button[type=submit][formmethod=post]") + await button.click() + await nextBody(page) - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.search, "?sort=asc") + assert.equal(pathname(page.url()), "/src/tests/fixtures/two.html") + assert.equal(await visitAction(page), "advance") +}) - await this.clickSelector("#form-action-self-q-b") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.search, "?q=b", "navigates without omitted keys") - } - - async "test standard GET form submission omits the [action] value's URLSearchParams from the submission"() { - await this.clickSelector("#form-action-self-submit") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.search, "") - } - - async "test standard GET form submission toggles submitter [disabled] attribute"() { - await this.clickSelector("#standard-get-form-submit") - - this.assert.equal( - await this.nextAttributeMutationNamed("standard-get-form-submit", "disabled"), - "", - "sets [disabled] on the submitter" - ) - this.assert.equal( - await this.nextAttributeMutationNamed("standard-get-form-submit", "disabled"), - null, - "removes [disabled] from the submitter" - ) - } - - async "test standard GET form submission appending keys"() { - await this.goToLocation("/src/tests/fixtures/form.html?query=1") - await this.clickSelector("#standard form.conflicting-values input[type=submit]") - await this.nextBody +test("test submitter POST form submission with multipart/form-data formenctype", async ({ page }) => { + await page.click("#submitter form[method=post]:not([enctype]) input[formenctype]") + await nextBeat() - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.getSearchParam("query"), "2") - } + const enctype = getSearchParam(page.url(), "enctype") + assert.ok(enctype?.startsWith("multipart/form-data"), "submits a multipart/form-data request") +}) - async "test standard form submission with empty created response"() { - const htmlBefore = await this.outerHTMLForSelector("body") - const button = await this.querySelector("#standard form.created input[type=submit]") - await button.click() - await this.nextBeat +test("test submitter GET submission from submitter with data-turbo-frame", async ({ page }) => { + await page.click("#submitter form[method=get] [type=submit][data-turbo-frame]") + await nextBeat() - const htmlAfter = await this.outerHTMLForSelector("body") - this.assert.equal(htmlAfter, htmlBefore) - } + const message = await page.locator("#frame div.message") + const title = await page.locator("h1") + assert.equal(await title.textContent(), "Form") + assert.equal(await message.textContent(), "Frame redirected") +}) - async "test standard form submission with empty no-content response"() { - const htmlBefore = await this.outerHTMLForSelector("body") - const button = await this.querySelector("#standard form.no-content input[type=submit]") - await button.click() - await this.nextBeat +test("test submitter POST submission from submitter with data-turbo-frame", async ({ page }) => { + await page.click("#submitter form[method=post] [type=submit][data-turbo-frame]") + await nextBeat() - const htmlAfter = await this.outerHTMLForSelector("body") - this.assert.equal(htmlAfter, htmlBefore) - } + const message = await page.locator("#frame div.message") + const title = await page.locator("h1") + assert.equal(await title.textContent(), "Form") + assert.equal(await message.textContent(), "Frame redirected") +}) + +test("test frame form GET submission from submitter with data-turbo-frame=_top", async ({ page }) => { + await page.click("#frame form[method=get] [type=submit][data-turbo-frame=_top]") + await nextBody(page) + + const title = await page.locator("h1") + assert.equal(await title.textContent(), "One") +}) + +test("test frame form POST submission from submitter with data-turbo-frame=_top", async ({ page }) => { + await page.click("#frame form[method=post] [type=submit][data-turbo-frame=_top]") + await nextBody(page) + + const title = await page.locator("h1") + assert.equal(await title.textContent(), "One") +}) + +test("test frame POST form targetting frame submission", async ({ page }) => { + await page.click("#targets-frame-post-form-submit") + + assert.ok(await formSubmitStarted(page), "fires turbo:submit-start") + + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") + + assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) + assert.equal("frame", fetchOptions.headers["Turbo-Frame"]) + + await nextEventNamed(page, "turbo:before-fetch-response") + + assert.ok(await formSubmitEnded(page), "fires turbo:submit-end") + + await nextEventNamed(page, "turbo:frame-render") + await nextEventNamed(page, "turbo:frame-load") + + const otherEvents = await readEventLogs(page) + assert.equal(otherEvents.length, 0, "no more events") + + const src = (await page.getAttribute("#frame", "src")) || "" + assert.equal(new URL(src).pathname, "/src/tests/fixtures/frames/frame.html") +}) + +test("test frame POST form targetting frame toggles submitter's [disabled] attribute", async ({ page }) => { + await page.click("#targets-frame-post-form-submit") + + assert.equal( + await nextAttributeMutationNamed(page, "targets-frame-post-form-submit", "disabled"), + "", + "sets [disabled] on the submitter" + ) + assert.equal( + await nextAttributeMutationNamed(page, "targets-frame-post-form-submit", "disabled"), + null, + "removes [disabled] from the submitter" + ) +}) + +test("test frame GET form targetting frame submission", async ({ page }) => { + await page.click("#targets-frame-get-form-submit") + + assert.ok(await formSubmitStarted(page), "fires turbo:submit-start") + + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") + + assert.notOk(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) + assert.equal("frame", fetchOptions.headers["Turbo-Frame"]) + + await nextEventNamed(page, "turbo:before-fetch-response") + + assert.ok(await formSubmitEnded(page), "fires turbo:submit-end") + + await nextEventNamed(page, "turbo:frame-render") + await nextEventNamed(page, "turbo:frame-load") + + const otherEvents = await readEventLogs(page) + assert.equal(otherEvents.length, 0, "no more events") + + const src = (await page.getAttribute("#frame", "src")) || "" + assert.equal(new URL(src).pathname, "/src/tests/fixtures/frames/frame.html") +}) + +test("test frame GET form targetting frame toggles submitter's [disabled] attribute", async ({ page }) => { + await page.click("#targets-frame-get-form-submit") + + assert.equal( + await nextAttributeMutationNamed(page, "targets-frame-get-form-submit", "disabled"), + "", + "sets [disabled] on the submitter" + ) + assert.equal( + await nextAttributeMutationNamed(page, "targets-frame-get-form-submit", "disabled"), + null, + "removes [disabled] from the submitter" + ) +}) + +test("test frame form GET submission from submitter referencing another frame", async ({ page }) => { + await page.click("#frame form[method=get] [type=submit][data-turbo-frame=hello]") + await nextBeat() + + const title = await page.locator("h1") + const frameTitle = await page.locator("#hello h2") + assert.equal(await frameTitle.textContent(), "Hello from a frame") + assert.equal(await title.textContent(), "Form") +}) + +test("test frame form POST submission from submitter referencing another frame", async ({ page }) => { + await page.click("#frame form[method=post] [type=submit][data-turbo-frame=hello]") + await nextBeat() + + const title = await page.locator("h1") + const frameTitle = await page.locator("#hello h2") + assert.equal(await frameTitle.textContent(), "Hello from a frame") + assert.equal(await title.textContent(), "Form") +}) + +test("test frame form submission with redirect response", async ({ page }) => { + const path = (await page.getAttribute("#frame form.redirect input[name=path]", "value")) || "" + const url = new URL(path, "http://localhost:9000") + url.searchParams.set("enctype", "application/x-www-form-urlencoded;charset=UTF-8") + + const button = await page.locator("#frame form.redirect input[type=submit]") + await button.click() + await nextBeat() + + const message = await page.locator("#frame div.message") + assert.notOk(await hasSelector(page, "#frame form.redirect")) + assert.equal(await message.textContent(), "Frame redirected") + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html", "does not redirect _top") + assert.notOk(search(page.url()), "does not redirect _top") + assert.equal(await page.getAttribute("#frame", "src"), url.href, "redirects the target frame") +}) + +test("test frame form submission toggles the ancestor frame's [aria-busy] attribute", async ({ page }) => { + await page.click("#frame form.redirect input[type=submit]") + await nextBeat() + + assert.equal(await nextAttributeMutationNamed(page, "frame", "busy"), "", "sets [busy] on the #frame") + assert.equal(await nextAttributeMutationNamed(page, "frame", "aria-busy"), "true", "sets [aria-busy] on the #frame") + assert.equal(await nextAttributeMutationNamed(page, "frame", "busy"), null, "removes [busy] from the #frame") + assert.equal( + await nextAttributeMutationNamed(page, "frame", "aria-busy"), + null, + "removes [aria-busy] from the #frame" + ) +}) + +test("test frame form submission toggles the target frame's [aria-busy] attribute", async ({ page }) => { + await page.click('#targets-frame form.frame [type="submit"]') + await nextBeat() + + assert.equal(await nextAttributeMutationNamed(page, "frame", "busy"), "", "sets [busy] on the #frame") + assert.equal(await nextAttributeMutationNamed(page, "frame", "aria-busy"), "true", "sets [aria-busy] on the #frame") + + const title = await page.locator("#frame h2") + assert.equal(await title.textContent(), "Frame: Loaded") + assert.equal(await nextAttributeMutationNamed(page, "frame", "busy"), null, "removes [busy] from the #frame") + assert.equal( + await nextAttributeMutationNamed(page, "frame", "aria-busy"), + null, + "removes [aria-busy] from the #frame" + ) +}) + +test("test frame form submission with empty created response", async ({ page }) => { + const htmlBefore = await outerHTMLForSelector(page, "#frame") + const button = await page.locator("#frame form.created input[type=submit]") + await button.click() + await nextBeat() + + const htmlAfter = await outerHTMLForSelector(page, "#frame") + assert.equal(htmlAfter, htmlBefore) +}) + +test("test frame form submission with empty no-content response", async ({ page }) => { + const htmlBefore = await outerHTMLForSelector(page, "#frame") + const button = await page.locator("#frame form.no-content input[type=submit]") + await button.click() + await nextBeat() + + const htmlAfter = await outerHTMLForSelector(page, "#frame") + assert.equal(htmlAfter, htmlBefore) +}) + +test("test frame form submission within a frame submits the Turbo-Frame header", async ({ page }) => { + await page.click("#frame form.redirect input[type=submit]") + + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") + + assert.ok(fetchOptions.headers["Turbo-Frame"], "submits with the Turbo-Frame header") +}) + +test("test invalid frame form submission with unprocessable entity status", async ({ page }) => { + await page.click("#frame form.unprocessable_entity input[type=submit]") + + assert.ok(await formSubmitStarted(page), "fires turbo:submit-start") + await nextEventNamed(page, "turbo:before-fetch-request") + await nextEventNamed(page, "turbo:before-fetch-response") + assert.ok(await formSubmitEnded(page), "fires turbo:submit-end") + await nextEventNamed(page, "turbo:frame-render") + await nextEventNamed(page, "turbo:frame-load") + + const otherEvents = await readEventLogs(page) + assert.equal(otherEvents.length, 0, "no more events") + + const title = await page.locator("#frame h2") + assert.ok(await hasSelector(page, "#reject form"), "only replaces frame") + assert.equal(await title.textContent(), "Frame: Unprocessable Entity") +}) + +test("test invalid frame form submission with internal server errror status", async ({ page }) => { + await page.click("#frame form.internal_server_error input[type=submit]") + + assert.ok(await formSubmitStarted(page), "fires turbo:submit-start") + await nextEventNamed(page, "turbo:before-fetch-request") + await nextEventNamed(page, "turbo:before-fetch-response") + assert.ok(await formSubmitEnded(page), "fires turbo:submit-end") + await nextEventNamed(page, "turbo:frame-render") + await nextEventNamed(page, "turbo:frame-load") + + const otherEvents = await readEventLogs(page) + assert.equal(otherEvents.length, 0, "no more events") + + assert.ok(await hasSelector(page, "#reject form"), "only replaces frame") + assert.equal(await page.textContent("#frame h2"), "Frame: Internal Server Error") +}) + +test("test frame form submission with stream response", async ({ page }) => { + const button = await page.locator("#frame form.stream[method=post] input[type=submit]") + await button.click() + await nextBeat() + + const message = await page.locator("#frame div.message") + assert.ok(await hasSelector(page, "#frame form.redirect")) + assert.equal(await message.textContent(), "Hello!") + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.notOk(await page.getAttribute("#frame", "src"), "does not change frame's src") +}) + +test("test frame form submission with HTTP verb other than GET or POST", async ({ page }) => { + await page.click("#frame form.put.stream input[type=submit]") + await nextBeat() + + assert.ok(await hasSelector(page, "#frame form.redirect")) + assert.equal(await page.textContent("#frame div.message"), "1: Hello!") + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") +}) + +test("test frame form submission with [data-turbo=false] on the form", async ({ page }) => { + await page.click('#frame form[data-turbo="false"] input[type=submit]') + await waitUntilSelector(page, "#element-id") + + assert.notOk(await formSubmitStarted(page)) +}) + +test("test frame form submission with [data-turbo=false] on the submitter", async ({ page }) => { + await page.click('#frame form:not([data-turbo]) input[data-turbo="false"]') + await waitUntilSelector(page, "#element-id") - async "test standard POST form submission with multipart/form-data enctype"() { - await this.clickSelector("#standard form[method=post][enctype] input[type=submit]") - await this.nextBeat - - const enctype = await this.getSearchParam("enctype") - this.assert.ok(enctype?.startsWith("multipart/form-data"), "submits a multipart/form-data request") - } - - async "test standard GET form submission ignores enctype"() { - await this.clickSelector("#standard form[method=get][enctype] input[type=submit]") - await this.nextBeat - - const enctype = await this.getSearchParam("enctype") - this.assert.notOk(enctype, "GET form submissions ignore enctype") - } - - async "test standard POST form submission without an enctype"() { - await this.clickSelector("#standard form[method=post].no-enctype input[type=submit]") - await this.nextBeat - - const enctype = await this.getSearchParam("enctype") - this.assert.ok( - enctype?.startsWith("application/x-www-form-urlencoded"), - "submits a application/x-www-form-urlencoded request" - ) - } - - async "test no-action form submission with single parameter"() { - await this.clickSelector("#no-action form.single input[type=submit]") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.getSearchParam("query"), "1") - - await this.clickSelector("#no-action form.single input[type=submit]") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.getSearchParam("query"), "1") - - await this.goToLocation("/src/tests/fixtures/form.html?query=2") - await this.clickSelector("#no-action form.single input[type=submit]") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.getSearchParam("query"), "1") - } - - async "test no-action form submission with multiple parameters"() { - await this.goToLocation("/src/tests/fixtures/form.html?query=2") - await this.clickSelector("#no-action form.multiple input[type=submit]") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.deepEqual(await this.getAllSearchParams("query"), ["1", "2"]) - - await this.clickSelector("#no-action form.multiple input[type=submit]") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.deepEqual(await this.getAllSearchParams("query"), ["1", "2"]) - } - - async "test no-action form submission submitter parameters"() { - await this.clickSelector("#no-action form.button-param [type=submit]") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.getSearchParam("query"), "1") - this.assert.deepEqual(await this.getAllSearchParams("button"), []) - - await this.clickSelector("#no-action form.button-param [type=submit]") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.getSearchParam("query"), "1") - this.assert.deepEqual(await this.getAllSearchParams("button"), []) - } - - async "test submitter with blank formaction submits to the current page"() { - await this.clickSelector("#blank-formaction button") - await this.nextBody - - this.assert.ok(await this.hasSelector("#blank-formaction"), "overrides form[action] navigation") - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - } - - async "test input named action with no action attribute"() { - await this.clickSelector("#action-input form.no-action [type=submit]") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.getSearchParam("action"), "1") - this.assert.equal(await this.getSearchParam("query"), "1") - } - - async "test input named action with action attribute"() { - await this.clickSelector("#action-input form.action [type=submit]") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.getSearchParam("action"), "1") - this.assert.equal(await this.getSearchParam("query"), "1") - } - - async "test invalid form submission with unprocessable entity status"() { - await this.clickSelector("#reject form.unprocessable_entity input[type=submit]") - await this.nextBody + assert.notOk(await formSubmitStarted(page)) +}) - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "Unprocessable Entity", "renders the response HTML") - this.assert.notOk(await this.hasSelector("#frame form.reject"), "replaces entire page") - } +test("test frame form submission ignores submissions with their defaultPrevented", async ({ page }) => { + await page.evaluate(() => document.addEventListener("submit", (event) => event.preventDefault(), true)) + await page.click("#frame .redirect [type=submit]") + await nextBeat() - async "test invalid form submission with long form"() { - await this.scrollToSelector("#reject form.unprocessable_entity_with_tall_form input[type=submit]") - await this.clickSelector("#reject form.unprocessable_entity_with_tall_form input[type=submit]") - await this.nextBody + assert.equal(await page.textContent("#frame h2"), "Frame: Form") + assert.equal(await page.getAttribute("#frame", "src"), null, "does not navigate frame") +}) - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "Unprocessable Entity", "renders the response HTML") - this.assert(await this.isScrolledToTop(), "page is scrolled to the top") - this.assert.notOk(await this.hasSelector("#frame form.reject"), "replaces entire page") - } - - async "test invalid form submission with server error status"() { - this.assert(await this.hasSelector("head > #form-fixture-styles")) - await this.clickSelector("#reject form.internal_server_error input[type=submit]") - await this.nextBody - - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "Internal Server Error", "renders the response HTML") - this.assert.notOk(await this.hasSelector("head > #form-fixture-styles"), "replaces head") - this.assert.notOk(await this.hasSelector("#frame form.reject"), "replaces entire page") - } +test("test form submission with [data-turbo=false] on the form", async ({ page }) => { + await page.click('#turbo-false form[data-turbo="false"] input[type=submit]') + await waitUntilSelector(page, "#element-id") - async "test submitter form submission reads button attributes"() { - const button = await this.querySelector("#submitter form button[type=submit]") - await button.click() - await this.nextBody + assert.notOk(await formSubmitStarted(page)) +}) - this.assert.equal(await this.pathname, "/src/tests/fixtures/two.html") - this.assert.equal(await this.visitAction, "advance") - } +test("test form submission with [data-turbo=false] on the submitter", async ({ page }) => { + await page.click('#turbo-false form:not([data-turbo]) input[data-turbo="false"]') + await waitUntilSelector(page, "#element-id") - async "test submitter POST form submission with multipart/form-data formenctype"() { - await this.clickSelector("#submitter form[method=post]:not([enctype]) input[formenctype]") - await this.nextBeat + assert.notOk(await formSubmitStarted(page)) +}) - const enctype = await this.getSearchParam("enctype") - this.assert.ok(enctype?.startsWith("multipart/form-data"), "submits a multipart/form-data request") - } +test("test form submission skipped within method=dialog", async ({ page }) => { + await page.click('#dialog-method [type="submit"]') + await nextBeat() - async "test submitter GET submission from submitter with data-turbo-frame"() { - await this.clickSelector("#submitter form[method=get] [type=submit][data-turbo-frame]") - await this.nextBeat + assert.notOk(await formSubmitStarted(page)) +}) - const message = await this.querySelector("#frame div.message") - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "Form") - this.assert.equal(await message.getVisibleText(), "Frame redirected") - } +test("test form submission skipped with submitter formmethod=dialog", async ({ page }) => { + await page.click('#dialog-formmethod-turbo-frame [formmethod="dialog"]') + await nextBeat() - async "test submitter POST submission from submitter with data-turbo-frame"() { - await this.clickSelector("#submitter form[method=post] [type=submit][data-turbo-frame]") - await this.nextBeat + assert.notOk(await formSubmitEnded(page)) +}) - const message = await this.querySelector("#frame div.message") - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "Form") - this.assert.equal(await message.getVisibleText(), "Frame redirected") - } - - async "test frame form GET submission from submitter with data-turbo-frame=_top"() { - await this.clickSelector("#frame form[method=get] [type=submit][data-turbo-frame=_top]") - await this.nextBody - - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "One") - } - - async "test frame form POST submission from submitter with data-turbo-frame=_top"() { - await this.clickSelector("#frame form[method=post] [type=submit][data-turbo-frame=_top]") - await this.nextBody - - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "One") - } - - async "test frame POST form targetting frame submission"() { - await this.clickSelector("#targets-frame-post-form-submit") - - this.assert.ok(await this.formSubmitStarted, "fires turbo:submit-start") - - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") - - this.assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) - this.assert.equal("frame", fetchOptions.headers["Turbo-Frame"]) - - await this.nextEventNamed("turbo:before-fetch-response") - - this.assert.ok(await this.formSubmitEnded, "fires turbo:submit-end") - - await this.nextEventNamed("turbo:frame-render") - await this.nextEventNamed("turbo:frame-load") - - const otherEvents = await this.eventLogChannel.read() - this.assert.equal(otherEvents.length, 0, "no more events") - - const src = (await (await this.querySelector("#frame")).getAttribute("src")) || "" - this.assert.equal(new URL(src).pathname, "/src/tests/fixtures/frames/frame.html") - } - - async "test frame POST form targetting frame toggles submitter's [disabled] attribute"() { - await this.clickSelector("#targets-frame-post-form-submit") - - this.assert.equal( - await this.nextAttributeMutationNamed("targets-frame-post-form-submit", "disabled"), - "", - "sets [disabled] on the submitter" - ) - this.assert.equal( - await this.nextAttributeMutationNamed("targets-frame-post-form-submit", "disabled"), - null, - "removes [disabled] from the submitter" - ) - } - - async "test frame GET form targetting frame submission"() { - await this.clickSelector("#targets-frame-get-form-submit") - - this.assert.ok(await this.formSubmitStarted, "fires turbo:submit-start") - - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") - - this.assert.notOk(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) - this.assert.equal("frame", fetchOptions.headers["Turbo-Frame"]) - - await this.nextEventNamed("turbo:before-fetch-response") - - this.assert.ok(await this.formSubmitEnded, "fires turbo:submit-end") - - await this.nextEventNamed("turbo:frame-render") - await this.nextEventNamed("turbo:frame-load") - - const otherEvents = await this.eventLogChannel.read() - this.assert.equal(otherEvents.length, 0, "no more events") - - const src = (await (await this.querySelector("#frame")).getAttribute("src")) || "" - this.assert.equal(new URL(src).pathname, "/src/tests/fixtures/frames/frame.html") - } - - async "test frame GET form targetting frame toggles submitter's [disabled] attribute"() { - await this.clickSelector("#targets-frame-get-form-submit") - - this.assert.equal( - await this.nextAttributeMutationNamed("targets-frame-get-form-submit", "disabled"), - "", - "sets [disabled] on the submitter" - ) - this.assert.equal( - await this.nextAttributeMutationNamed("targets-frame-get-form-submit", "disabled"), - null, - "removes [disabled] from the submitter" - ) - } - - async "test frame form GET submission from submitter referencing another frame"() { - await this.clickSelector("#frame form[method=get] [type=submit][data-turbo-frame=hello]") - await this.nextBeat - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#hello h2") - this.assert.equal(await frameTitle.getVisibleText(), "Hello from a frame") - this.assert.equal(await title.getVisibleText(), "Form") - } - - async "test frame form POST submission from submitter referencing another frame"() { - await this.clickSelector("#frame form[method=post] [type=submit][data-turbo-frame=hello]") - await this.nextBeat - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#hello h2") - this.assert.equal(await frameTitle.getVisibleText(), "Hello from a frame") - this.assert.equal(await title.getVisibleText(), "Form") - } - - async "test frame form submission with redirect response"() { - const path = (await this.attributeForSelector("#frame form.redirect input[name=path]", "value")) || "" - const url = new URL(path, "http://localhost:9000") - url.searchParams.set("enctype", "application/x-www-form-urlencoded;charset=UTF-8") - - const button = await this.querySelector("#frame form.redirect input[type=submit]") - await button.click() - await this.nextBeat - - const message = await this.querySelector("#frame div.message") - this.assert.notOk(await this.hasSelector("#frame form.redirect")) - this.assert.equal(await message.getVisibleText(), "Frame redirected") - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html", "does not redirect _top") - this.assert.notOk(await this.search, "does not redirect _top") - this.assert.equal(await this.attributeForSelector("#frame", "src"), url.href, "redirects the target frame") - } - - async "test frame form submission toggles the ancestor frame's [aria-busy] attribute"() { - await this.clickSelector("#frame form.redirect input[type=submit]") - await this.nextBeat - - this.assert.equal(await this.nextAttributeMutationNamed("frame", "busy"), "", "sets [busy] on the #frame") - this.assert.equal( - await this.nextAttributeMutationNamed("frame", "aria-busy"), - "true", - "sets [aria-busy] on the #frame" - ) - this.assert.equal(await this.nextAttributeMutationNamed("frame", "busy"), null, "removes [busy] from the #frame") - this.assert.equal( - await this.nextAttributeMutationNamed("frame", "aria-busy"), - null, - "removes [aria-busy] from the #frame" - ) - } - - async "test frame form submission toggles the target frame's [aria-busy] attribute"() { - await this.clickSelector('#targets-frame form.frame [type="submit"]') - await this.nextBeat - - this.assert.equal(await this.nextAttributeMutationNamed("frame", "busy"), "", "sets [busy] on the #frame") - this.assert.equal( - await this.nextAttributeMutationNamed("frame", "aria-busy"), - "true", - "sets [aria-busy] on the #frame" - ) - - const title = await this.querySelector("#frame h2") - this.assert.equal(await title.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.nextAttributeMutationNamed("frame", "busy"), null, "removes [busy] from the #frame") - this.assert.equal( - await this.nextAttributeMutationNamed("frame", "aria-busy"), - null, - "removes [aria-busy] from the #frame" - ) - } - - async "test frame form submission with empty created response"() { - const htmlBefore = await this.outerHTMLForSelector("#frame") - const button = await this.querySelector("#frame form.created input[type=submit]") - await button.click() - await this.nextBeat - - const htmlAfter = await this.outerHTMLForSelector("#frame") - this.assert.equal(htmlAfter, htmlBefore) - } - - async "test frame form submission with empty no-content response"() { - const htmlBefore = await this.outerHTMLForSelector("#frame") - const button = await this.querySelector("#frame form.no-content input[type=submit]") - await button.click() - await this.nextBeat - - const htmlAfter = await this.outerHTMLForSelector("#frame") - this.assert.equal(htmlAfter, htmlBefore) - } - - async "test frame form submission within a frame submits the Turbo-Frame header"() { - await this.clickSelector("#frame form.redirect input[type=submit]") - - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") - - this.assert.ok(fetchOptions.headers["Turbo-Frame"], "submits with the Turbo-Frame header") - } - - async "test invalid frame form submission with unprocessable entity status"() { - await this.clickSelector("#frame form.unprocessable_entity input[type=submit]") - - this.assert.ok(await this.formSubmitStarted, "fires turbo:submit-start") - await this.nextEventNamed("turbo:before-fetch-request") - await this.nextEventNamed("turbo:before-fetch-response") - this.assert.ok(await this.formSubmitEnded, "fires turbo:submit-end") - await this.nextEventNamed("turbo:frame-render") - await this.nextEventNamed("turbo:frame-load") - - const otherEvents = await this.eventLogChannel.read() - this.assert.equal(otherEvents.length, 0, "no more events") - - const title = await this.querySelector("#frame h2") - this.assert.ok(await this.hasSelector("#reject form"), "only replaces frame") - this.assert.equal(await title.getVisibleText(), "Frame: Unprocessable Entity") - } - - async "test invalid frame form submission with internal server errror status"() { - await this.clickSelector("#frame form.internal_server_error input[type=submit]") - - this.assert.ok(await this.formSubmitStarted, "fires turbo:submit-start") - await this.nextEventNamed("turbo:before-fetch-request") - await this.nextEventNamed("turbo:before-fetch-response") - this.assert.ok(await this.formSubmitEnded, "fires turbo:submit-end") - await this.nextEventNamed("turbo:frame-render") - await this.nextEventNamed("turbo:frame-load") - - const otherEvents = await this.eventLogChannel.read() - this.assert.equal(otherEvents.length, 0, "no more events") - - const title = await this.querySelector("#frame h2") - this.assert.ok(await this.hasSelector("#reject form"), "only replaces frame") - this.assert.equal(await title.getVisibleText(), "Frame: Internal Server Error") - } - - async "test frame form submission with stream response"() { - const button = await this.querySelector("#frame form.stream input[type=submit]") - await button.click() - await this.nextBeat - - const message = await this.querySelector("#frame div.message") - this.assert.ok(await this.hasSelector("#frame form.redirect")) - this.assert.equal(await message.getVisibleText(), "Hello!") - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.notOk(await this.propertyForSelector("#frame", "src"), "does not change frame's src") - } - - async "test frame form submission with HTTP verb other than GET or POST"() { - await this.clickSelector("#frame form.put.stream input[type=submit]") - await this.nextBeat - - const message = await this.querySelector("#frame div.message") - this.assert.ok(await this.hasSelector("#frame form.redirect")) - this.assert.equal(await message.getVisibleText(), "1: Hello!") - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - } - - async "test frame form submission with [data-turbo=false] on the form"() { - await this.clickSelector('#frame form[data-turbo="false"] input[type=submit]') - await this.nextBody - await this.querySelector("#element-id") - - this.assert.notOk(await this.formSubmitStarted) - } - - async "test frame form submission with [data-turbo=false] on the submitter"() { - await this.clickSelector('#frame form:not([data-turbo]) input[data-turbo="false"]') - await this.nextBody - await this.querySelector("#element-id") - - this.assert.notOk(await this.formSubmitStarted) - } - - async "test frame form submission ignores submissions with their defaultPrevented"() { - await this.evaluate(() => document.addEventListener("submit", (event) => event.preventDefault(), true)) - await this.clickSelector("#frame .redirect [type=submit]") - await this.nextBeat - - this.assert.equal(await (await this.querySelector("#frame h2")).getVisibleText(), "Frame: Form") - this.assert.equal(await this.attributeForSelector("#frame", "src"), null, "does not navigate frame") - } +test("test form submission targetting frame skipped within method=dialog", async ({ page }) => { + await page.click("#dialog-method-turbo-frame button") + await nextBeat() - async "test form submission with [data-turbo=false] on the form"() { - await this.clickSelector('#turbo-false form[data-turbo="false"] input[type=submit]') - await this.nextBody - await this.querySelector("#element-id") + assert.notOk(await formSubmitEnded(page)) +}) - this.assert.notOk(await this.formSubmitStarted) - } +test("test form submission targetting frame skipped with submitter formmethod=dialog", async ({ page }) => { + await page.click('#dialog-formmethod [formmethod="dialog"]') + await nextBeat() - async "test form submission with [data-turbo=false] on the submitter"() { - await this.clickSelector('#turbo-false form:not([data-turbo]) input[data-turbo="false"]') - await this.nextBody - await this.querySelector("#element-id") + assert.notOk(await formSubmitStarted(page)) +}) - this.assert.notOk(await this.formSubmitStarted) - } +test("test form submission targets disabled frame", async ({ page }) => { + await page.evaluate(() => document.getElementById("frame")?.setAttribute("disabled", "")) + await page.click('#targets-frame form.one [type="submit"]') + await nextBody(page) - async "test form submission skipped within method=dialog"() { - await this.clickSelector('#dialog-method [type="submit"]') - await this.nextBeat + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") +}) - this.assert.notOk(await this.formSubmitStarted) - } +test("test form submission targeting a frame submits the Turbo-Frame header", async ({ page }) => { + await page.click('#targets-frame [type="submit"]') - async "test form submission skipped with submitter formmethod=dialog"() { - await this.clickSelector('#dialog-formmethod-turbo-frame [formmethod="dialog"]') - await this.nextBeat + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") - this.assert.notOk(await this.formSubmitEnded) - } + assert.ok(fetchOptions.headers["Turbo-Frame"], "submits with the Turbo-Frame header") +}) - async "test form submission targetting frame skipped within method=dialog"() { - await this.clickSelector("#dialog-method-turbo-frame button") - await this.nextBeat +test("test link method form submission inside frame", async ({ page }) => { + await page.click("#link-method-inside-frame") + await nextBeat() - this.assert.notOk(await this.formSubmitEnded) - } + assert.equal(await await page.textContent("#frame h2"), "Frame: Loaded") + assert.notOk(await hasSelector(page, "#nested-child")) +}) - async "test form submission targetting frame skipped with submitter formmethod=dialog"() { - await this.clickSelector('#dialog-formmethod [formmethod="dialog"]') - await this.nextBeat +test("test link method form submission inside frame with data-turbo-frame=_top", async ({ page }) => { + await page.click("#link-method-inside-frame-target-top") + await nextBody(page) - this.assert.notOk(await this.formSubmitStarted) - } + const title = await page.locator("h1") + assert.equal(await title.textContent(), "Hello") +}) - async "test form submission targets disabled frame"() { - await this.remote.execute(() => document.getElementById("frame")?.setAttribute("disabled", "")) - await this.clickSelector('#targets-frame form.one [type="submit"]') - await this.nextBody +test("test link method form submission inside frame with data-turbo-frame target", async ({ page }) => { + await page.click("#link-method-inside-frame-with-target") + await nextBeat() - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - } + const title = await page.locator("h1") + const frameTitle = await page.locator("#hello h2") + assert.equal(await frameTitle.textContent(), "Hello from a frame") + assert.equal(await title.textContent(), "Form") +}) - async "test form submission targeting a frame submits the Turbo-Frame header"() { - await this.clickSelector('#targets-frame [type="submit"]') +test("test stream link method form submission inside frame", async ({ page }) => { + await page.click("#stream-link-method-inside-frame") + await nextBeat() - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") + const message = page.locator("#frame div.message") + assert.equal(await message.textContent(), "Link!") +}) - this.assert.ok(fetchOptions.headers["Turbo-Frame"], "submits with the Turbo-Frame header") - } +test("test stream link GET method form submission inside frame", async ({ page }) => { + await page.click("#stream-link-get-method-inside-frame") - async "test link method form submission inside frame"() { - await this.clickSelector("#link-method-inside-frame") - await this.nextBeat + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") - const title = await this.querySelector("#frame h2") - this.assert.equal(await title.getVisibleText(), "Frame: Loaded") - this.assert.notOk(await this.hasSelector("#nested-child")) - } + assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) +}) - async "test link method form submission inside frame with data-turbo-frame=_top"() { - await this.clickSelector("#link-method-inside-frame-target-top") - await this.nextBody +test("test stream link inside frame", async ({ page }) => { + await page.click("#stream-link-inside-frame") - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "Hello") - } + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") - async "test link method form submission inside frame with data-turbo-frame target"() { - await this.clickSelector("#link-method-inside-frame-with-target") - await this.nextBeat + assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) +}) - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#hello h2") - this.assert.equal(await frameTitle.getVisibleText(), "Hello from a frame") - this.assert.equal(await title.getVisibleText(), "Form") - } +test("test stream link outside frame", async ({ page }) => { + await page.click("#stream-link-outside-frame") - async "test stream link method form submission inside frame"() { - await this.clickSelector("#stream-link-method-inside-frame") - await this.nextBeat + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") - const message = await this.querySelector("#frame div.message") - this.assert.equal(await message.getVisibleText(), "Link!") - } + assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) +}) - async "test stream link GET method form submission inside frame"() { - await this.clickSelector("#stream-link-get-method-inside-frame") +test("test link method form submission within form inside frame", async ({ page }) => { + await page.click("#stream-link-method-within-form-inside-frame") + await nextBeat() - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") + const message = page.locator("#frame div.message") + assert.equal(await message.textContent(), "Link!") +}) - this.assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) - } +test("test link method form submission inside frame with confirmation confirmed", async ({ page }) => { + page.on("dialog", (dialog) => { + assert.equal(dialog.message(), "Are you sure?") + dialog.accept() + }) - async "test stream link inside frame"() { - await this.clickSelector("#stream-link-inside-frame") + await page.click("#link-method-inside-frame-with-confirmation") + await nextBeat() - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") + const message = page.locator("#frame div.message") + assert.equal(await message.textContent(), "Link!") +}) - this.assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) - } +test("test link method form submission inside frame with confirmation cancelled", async ({ page }) => { + page.on("dialog", (dialog) => { + assert.equal(dialog.message(), "Are you sure?") + dialog.dismiss() + }) - async "test stream link outside frame"() { - await this.clickSelector("#stream-link-outside-frame") + await page.click("#link-method-inside-frame-with-confirmation") + await nextBeat() - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") + assert.notOk(await hasSelector(page, "#frame div.message"), "Not confirming form submission does not submit the form") +}) - this.assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) - } +test("test link method form submission outside frame", async ({ page }) => { + await page.click("#link-method-outside-frame") + await nextBody(page) - async "test link method form submission within form inside frame"() { - await this.clickSelector("#stream-link-method-within-form-inside-frame") - await this.nextBeat + const title = await page.locator("h1") + assert.equal(await title.textContent(), "Hello") +}) - const message = await this.querySelector("#frame div.message") - this.assert.equal(await message.getVisibleText(), "Link!") - } +test("test stream link method form submission outside frame", async ({ page }) => { + await page.click("#stream-link-method-outside-frame") + await nextBeat() - async "test link method form submission inside frame with confirmation confirmed"() { - await this.clickSelector("#link-method-inside-frame-with-confirmation") + const message = page.locator("#frame div.message") + assert.equal(await message.textContent(), "Link!") +}) - this.assert.equal(await this.getAlertText(), "Are you sure?") - await this.acceptAlert() +test("test link method form submission within form outside frame", async ({ page }) => { + await page.click("#link-method-within-form-outside-frame") + await nextBody(page) - await this.nextBeat + const title = await page.locator("h1") + assert.equal(await title.textContent(), "Hello") +}) - const message = await this.querySelector("#frame div.message") - this.assert.equal(await message.getVisibleText(), "Link!") - } +test("test stream link method form submission within form outside frame", async ({ page }) => { + await page.click("#stream-link-method-within-form-outside-frame") + await nextBeat() - async "test link method form submission inside frame with confirmation cancelled"() { - await this.clickSelector("#link-method-inside-frame-with-confirmation") + assert.equal(await page.textContent("#frame div.message"), "Link!") +}) - this.assert.equal(await this.getAlertText(), "Are you sure?") - await this.dismissAlert() +test("test form submission with form mode off", async ({ page }) => { + await page.evaluate(() => window.Turbo.setFormMode("off")) + await page.click("#standard form.turbo-enabled input[type=submit]") - await this.nextBeat + assert.notOk(await formSubmitStarted(page)) +}) - this.assert.notOk( - await this.hasSelector("#frame div.message"), - "Not confirming form submission does not submit the form" - ) - } +test("test form submission with form mode optin and form not enabled", async ({ page }) => { + await page.evaluate(() => window.Turbo.setFormMode("optin")) + await page.click("#standard form.redirect input[type=submit]") - async "test link method form submission outside frame"() { - await this.clickSelector("#link-method-outside-frame") - await this.nextBody + assert.notOk(await formSubmitStarted(page)) +}) - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "Hello") - } +test("test form submission with form mode optin and form enabled", async ({ page }) => { + await page.evaluate(() => window.Turbo.setFormMode("optin")) + await page.click("#standard form.turbo-enabled input[type=submit]") - async "test stream link method form submission outside frame"() { - await this.clickSelector("#stream-link-method-outside-frame") - await this.nextBeat + assert.ok(await formSubmitStarted(page)) +}) - const message = await this.querySelector("#frame div.message") - this.assert.equal(await message.getVisibleText(), "Link!") - } +test("test turbo:before-fetch-request fires on the form element", async ({ page }) => { + await page.click('#targets-frame form.one [type="submit"]') + assert.ok(await nextEventOnTarget(page, "form_one", "turbo:before-fetch-request")) +}) - async "test link method form submission within form outside frame"() { - await this.clickSelector("#link-method-within-form-outside-frame") - await this.nextBody +test("test turbo:before-fetch-response fires on the form element", async ({ page }) => { + await page.click('#targets-frame form.one [type="submit"]') + assert.ok(await nextEventOnTarget(page, "form_one", "turbo:before-fetch-response")) +}) - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "Hello") - } +test("test POST to external action ignored", async ({ page }) => { + await page.click("#submit-external") + await noNextEventNamed(page, "turbo:before-fetch-request") + await nextBody(page) - async "test stream link method form submission within form outside frame"() { - await this.clickSelector("#stream-link-method-within-form-outside-frame") - await this.nextBeat + assert.equal(page.url(), "https://httpbin.org/post") +}) - const message = await this.querySelector("#frame div.message") - this.assert.equal(await message.getVisibleText(), "Link!") - } +test("test POST to external action within frame ignored", async ({ page }) => { + await page.click("#submit-external-within-ignored") + await noNextEventNamed(page, "turbo:before-fetch-request") + await nextBody(page) - async "test form submission with form mode off"() { - await this.remote.execute(() => window.Turbo.setFormMode("off")) - await this.clickSelector("#standard form.turbo-enabled input[type=submit]") + assert.equal(page.url(), "https://httpbin.org/post") +}) - this.assert.notOk(await this.formSubmitStarted) - } +test("test POST to external action targetting frame ignored", async ({ page }) => { + await page.click("#submit-external-target-ignored") + await noNextEventNamed(page, "turbo:before-fetch-request") + await nextBody(page) - async "test form submission with form mode optin and form not enabled"() { - await this.remote.execute(() => window.Turbo.setFormMode("optin")) - await this.clickSelector("#standard form.redirect input[type=submit]") + assert.equal(page.url(), "https://httpbin.org/post") +}) - this.assert.notOk(await this.formSubmitStarted) - } - - async "test form submission with form mode optin and form enabled"() { - await this.remote.execute(() => window.Turbo.setFormMode("optin")) - await this.clickSelector("#standard form.turbo-enabled input[type=submit]") - - this.assert.ok(await this.formSubmitStarted) - } - - async "test turbo:before-fetch-request fires on the form element"() { - await this.clickSelector('#targets-frame form.one [type="submit"]') - this.assert.ok(await this.nextEventOnTarget("form_one", "turbo:before-fetch-request")) - } - - async "test turbo:before-fetch-response fires on the form element"() { - await this.clickSelector('#targets-frame form.one [type="submit"]') - this.assert.ok(await this.nextEventOnTarget("form_one", "turbo:before-fetch-response")) - } - - async "test POST to external action ignored"() { - await this.clickSelector("#submit-external") - await this.noNextEventNamed("turbo:before-fetch-request") - await this.nextBody - - this.assert.equal(await this.location, "https://httpbin.org/post") - } - - async "test POST to external action within frame ignored"() { - await this.clickSelector("#submit-external-within-ignored") - await this.noNextEventNamed("turbo:before-fetch-request") - await this.nextBody - - this.assert.equal(await this.location, "https://httpbin.org/post") - } - - async "test POST to external action targetting frame ignored"() { - await this.clickSelector("#submit-external-target-ignored") - await this.noNextEventNamed("turbo:before-fetch-request") - await this.nextBody - - this.assert.equal(await this.location, "https://httpbin.org/post") - } - - get formSubmitStarted() { - return this.getFromLocalStorage("formSubmitStarted") - } - - get formSubmitEnded() { - return this.getFromLocalStorage("formSubmitEnded") - } +function formSubmitStarted(page: Page) { + return getFromLocalStorage(page, "formSubmitStarted") } -FormSubmissionTests.registerSuite() +function formSubmitEnded(page: Page) { + return getFromLocalStorage(page, "formSubmitEnded") +} diff --git a/src/tests/functional/frame_navigation_tests.ts b/src/tests/functional/frame_navigation_tests.ts index 513edb5bb..212b709c5 100644 --- a/src/tests/functional/frame_navigation_tests.ts +++ b/src/tests/functional/frame_navigation_tests.ts @@ -1,27 +1,24 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { nextEventOnTarget } from "../helpers/page" -export class FrameNavigationTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/frame_navigation.html") - } +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/frame_navigation.html") +}) - async "test frame navigation with descendant link"() { - await this.clickSelector("#inside") +test("test frame navigation with descendant link", async ({ page }) => { + await page.click("#inside") - await this.nextEventOnTarget("frame", "turbo:frame-load") - } + await nextEventOnTarget(page, "frame", "turbo:frame-load") +}) - async "test frame navigation with self link"() { - await this.clickSelector("#self") +test("test frame navigation with self link", async ({ page }) => { + await page.click("#self") - await this.nextEventOnTarget("frame", "turbo:frame-load") - } + await nextEventOnTarget(page, "frame", "turbo:frame-load") +}) - async "test frame navigation with exterior link"() { - await this.clickSelector("#outside") +test("test frame navigation with exterior link", async ({ page }) => { + await page.click("#outside") - await this.nextEventOnTarget("frame", "turbo:frame-load") - } -} - -FrameNavigationTests.registerSuite() + await nextEventOnTarget(page, "frame", "turbo:frame-load") +}) diff --git a/src/tests/functional/frame_tests.ts b/src/tests/functional/frame_tests.ts index a9afd1316..0f0ec4045 100644 --- a/src/tests/functional/frame_tests.ts +++ b/src/tests/functional/frame_tests.ts @@ -1,683 +1,703 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" - -export class FrameTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/frames.html") - } - - async "test navigating a frame a second time does not leak event listeners"() { - await this.withoutChangingEventListenersCount(async () => { - await this.clickSelector("#outer-frame-link") - await this.nextEventOnTarget("frame", "turbo:frame-load") - await this.clickSelector("#outside-frame-form") - await this.nextEventOnTarget("frame", "turbo:frame-load") - await this.clickSelector("#outer-frame-link") - await this.nextEventOnTarget("frame", "turbo:frame-load") - }) - } - - async "test following a link preserves the current element's attributes"() { - const currentPath = await this.pathname - - await this.clickSelector("#hello a") - await this.nextBeat - - const frame = await this.querySelector("turbo-frame#frame") - this.assert.equal(await frame.getAttribute("data-loaded-from"), currentPath) - this.assert.equal(await frame.getAttribute("src"), await this.propertyForSelector("#hello a", "href")) - } - - async "test following a link sets the frame element's [src]"() { - await this.clickSelector("#link-frame-with-search-params") - - const { url } = await this.nextEventOnTarget("frame", "turbo:before-fetch-request") - const fetchRequestUrl = new URL(url) - - this.assert.equal(fetchRequestUrl.pathname, "/src/tests/fixtures/frames/frame.html") - this.assert.equal(fetchRequestUrl.searchParams.get("key"), "value", "fetch request encodes query parameters") - - await this.nextBeat - const src = new URL((await this.attributeForSelector("#frame", "src")) || "") - - this.assert.equal(src.pathname, "/src/tests/fixtures/frames/frame.html") - this.assert.equal(src.searchParams.get("key"), "value", "[src] attribute encodes query parameters") - } - - async "test a frame whose src references itself does not infinitely loop"() { - await this.clickSelector("#frame-self") - - await this.nextEventOnTarget("frame", "turbo:frame-render") - await this.nextEventOnTarget("frame", "turbo:frame-load") - - const otherEvents = await this.eventLogChannel.read() - this.assert.equal(otherEvents.length, 0, "no more events") - } - - async "test following a link driving a frame toggles the [aria-busy=true] attribute"() { - await this.clickSelector("#hello a") - - this.assert.equal(await this.nextAttributeMutationNamed("frame", "busy"), "", "sets [busy] on the #frame") - this.assert.equal( - await this.nextAttributeMutationNamed("frame", "aria-busy"), - "true", - "sets [aria-busy=true] on the #frame" - ) - this.assert.equal(await this.nextAttributeMutationNamed("frame", "busy"), null, "removes [busy] on the #frame") - this.assert.equal( - await this.nextAttributeMutationNamed("frame", "aria-busy"), - null, - "removes [aria-busy] from the #frame" - ) - } - - async "test following a link to a page without a matching frame results in an empty frame"() { - await this.clickSelector("#missing a") - await this.nextBeat - this.assert.notOk(await this.innerHTMLForSelector("#missing")) - } - - async "test following a link within a frame with a target set navigates the target frame"() { - await this.clickSelector("#hello a") - await this.nextBeat - - const frameText = await this.querySelector("#frame h2") - this.assert.equal(await frameText.getVisibleText(), "Frame: Loaded") - } - - async "test following a link in rapid succession cancels the previous request"() { - await this.clickSelector("#outside-frame-form") - await this.clickSelector("#outer-frame-link") - await this.nextBeat - - const frameText = await this.querySelector("#frame h2") - this.assert.equal(await frameText.getVisibleText(), "Frame: Loaded") - } - - async "test following a link within a descendant frame whose ancestor declares a target set navigates the descendant frame"() { - const link = await this.querySelector("#nested-root[target=frame] #nested-child a:not([data-turbo-frame])") - const href = await link.getProperty("href") - - await link.click() - await this.nextBeat - - const frame = await this.querySelector("#frame h2") - const nestedRoot = await this.querySelector("#nested-root h2") - const nestedChild = await this.querySelector("#nested-child") - this.assert.equal(await frame.getVisibleText(), "Frames: #frame") - this.assert.equal(await nestedRoot.getVisibleText(), "Frames: #nested-root") - this.assert.equal(await nestedChild.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.attributeForSelector("#frame", "src"), null) - this.assert.equal(await this.attributeForSelector("#nested-root", "src"), null) - this.assert.equal(await this.attributeForSelector("#nested-child", "src"), href) - } - - async "test following a link that declares data-turbo-frame within a frame whose ancestor respects the override"() { - await this.clickSelector("#nested-root[target=frame] #nested-child a[data-turbo-frame]") - await this.nextBeat - - const frameText = await this.querySelector("body > h1") - this.assert.equal(await frameText.getVisibleText(), "One") - this.assert.notOk(await this.hasSelector("#frame")) - this.assert.notOk(await this.hasSelector("#nested-root")) - this.assert.notOk(await this.hasSelector("#nested-child")) - } - - async "test following a form within a nested frame with form target top"() { - await this.clickSelector("#nested-child-navigate-form-top-submit") - await this.nextBeat - - const frameText = await this.querySelector("body > h1") - this.assert.equal(await frameText.getVisibleText(), "One") - this.assert.notOk(await this.hasSelector("#frame")) - this.assert.notOk(await this.hasSelector("#nested-root")) - this.assert.notOk(await this.hasSelector("#nested-child")) - } - - async "test following a form within a nested frame with child frame target top"() { - await this.clickSelector("#nested-child-navigate-top-submit") - await this.nextBeat - - const frameText = await this.querySelector("body > h1") - this.assert.equal(await frameText.getVisibleText(), "One") - this.assert.notOk(await this.hasSelector("#frame")) - this.assert.notOk(await this.hasSelector("#nested-root")) - this.assert.notOk(await this.hasSelector("#nested-child-navigate-top")) - } - - async "test following a link within a frame with target=_top navigates the page"() { - this.assert.equal(await this.attributeForSelector("#navigate-top", "src"), null) - - await this.clickSelector("#navigate-top a:not([data-turbo-frame])") - await this.nextBeat - - const frameText = await this.querySelector("body > h1") - this.assert.equal(await frameText.getVisibleText(), "One") - this.assert.notOk(await this.hasSelector("#navigate-top a")) - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.getSearchParam("key"), "value") - } - - async "test following a link that declares data-turbo-frame='_self' within a frame with target=_top navigates the frame itself"() { - this.assert.equal(await this.attributeForSelector("#navigate-top", "src"), null) - - await this.clickSelector("#navigate-top a[data-turbo-frame='_self']") - await this.nextBeat - - const title = await this.querySelector("body > h1") - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.ok(await this.hasSelector("#navigate-top")) - const frame = await this.querySelector("#navigate-top") - this.assert.equal(await frame.getVisibleText(), "Replaced only the frame") - } - - async "test following a link to a page with a which lazily loads a matching frame"() { - await this.nextBeat - await this.clickSelector("#recursive summary") - this.assert.ok(await this.querySelector("#recursive details[open]")) - - await this.clickSelector("#recursive a") - await this.nextBeat - this.assert.ok(await this.querySelector("#recursive details:not([open])")) - } - - async "test submitting a form that redirects to a page with a which lazily loads a matching frame"() { - await this.nextBeat - await this.clickSelector("#recursive summary") - this.assert.ok(await this.querySelector("#recursive details[open]")) - - await this.clickSelector("#recursive input[type=submit]") - await this.nextBeat - this.assert.ok(await this.querySelector("#recursive details:not([open])")) - } - - async "test removing [disabled] attribute from eager-loaded frame navigates it"() { - await this.remote.execute(() => document.getElementById("frame")?.setAttribute("disabled", "")) - await this.remote.execute(() => - document.getElementById("frame")?.setAttribute("src", "/src/tests/fixtures/frames/frame.html") - ) - - this.assert.ok( - await this.noNextEventNamed("turbo:before-fetch-request"), - "[disabled] frames do not submit requests" - ) - - await this.remote.execute(() => document.getElementById("frame")?.removeAttribute("disabled")) - - await this.nextEventNamed("turbo:before-fetch-request") - } - - async "test evaluates frame script elements on each render"() { - this.assert.equal(await this.frameScriptEvaluationCount, undefined) - - this.clickSelector("#body-script-link") - await this.sleep(200) - this.assert.equal(await this.frameScriptEvaluationCount, 1) - - this.clickSelector("#body-script-link") - await this.sleep(200) - this.assert.equal(await this.frameScriptEvaluationCount, 2) - } - - async "test does not evaluate data-turbo-eval=false scripts"() { - this.clickSelector("#eval-false-script-link") - await this.nextBeat - this.assert.equal(await this.frameScriptEvaluationCount, undefined) - } - - async "test redirecting in a form is still navigatable after redirect"() { - await this.nextBeat - await this.clickSelector("#navigate-form-redirect") - await this.nextBeat - this.assert.ok(await this.querySelector("#form-redirect")) - - await this.nextBeat - await this.clickSelector("#submit-form") - await this.nextBeat - this.assert.ok(await this.querySelector("#form-redirected-header")) - - await this.nextBeat - await this.clickSelector("#navigate-form-redirect") - await this.nextBeat - this.assert.ok(await this.querySelector("#form-redirect-header")) - } - - async "test 'turbo:frame-render' is triggered after frame has finished rendering"() { - await this.clickSelector("#frame-part") - - await this.nextEventNamed("turbo:frame-render") // recursive - const { fetchResponse } = await this.nextEventNamed("turbo:frame-render") - - this.assert.include(fetchResponse.response.url, "/src/tests/fixtures/frames/part.html") - } - - async "test navigating a frame fires events"() { - await this.clickSelector("#outside-frame-form") - - const { fetchResponse } = await this.nextEventOnTarget("frame", "turbo:frame-render") - this.assert.include(fetchResponse.response.url, "/src/tests/fixtures/frames/form.html") - - await this.nextEventOnTarget("frame", "turbo:frame-load") - - const otherEvents = await this.eventLogChannel.read() - this.assert.equal(otherEvents.length, 0, "no more events") - } - - async "test following inner link reloads frame on every click"() { - await this.clickSelector("#hello a") - await this.nextEventNamed("turbo:before-fetch-request") - - await this.clickSelector("#hello a") - await this.nextEventNamed("turbo:before-fetch-request") - } - - async "test following outer link reloads frame on every click"() { - await this.clickSelector("#outer-frame-link") - await this.nextEventNamed("turbo:before-fetch-request") - - await this.clickSelector("#outer-frame-link") - await this.nextEventNamed("turbo:before-fetch-request") - } - - async "test following outer form reloads frame on every submit"() { - await this.clickSelector("#outer-frame-submit") - await this.nextEventNamed("turbo:before-fetch-request") +import { Page, test } from "@playwright/test" +import { assert, Assertion } from "chai" +import { + attributeForSelector, + hasSelector, + innerHTMLForSelector, + nextAttributeMutationNamed, + nextBeat, + nextEventNamed, + nextEventOnTarget, + noNextEventNamed, + noNextEventOnTarget, + pathname, + propertyForSelector, + readEventLogs, + scrollPosition, + scrollToSelector, + searchParams, +} from "../helpers/page" + +assert.equal = function (actual: any, expected: any, message?: string) { + actual = typeof actual == "string" ? actual.trim() : actual + expected = typeof expected == "string" ? expected.trim() : expected + + const assertExpectation = new Assertion(expected) + + assertExpectation.to.equal(expected, message) +} - await this.clickSelector("#outer-frame-submit") - await this.nextEventNamed("turbo:before-fetch-request") - } +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/frames.html") + await readEventLogs(page) +}) + +test("test navigating a frame a second time does not leak event listeners", async ({ page }) => { + await withoutChangingEventListenersCount(page, async () => { + await page.click("#outer-frame-link") + await nextEventOnTarget(page, "frame", "turbo:frame-load") + await page.click("#outside-frame-form") + await nextEventOnTarget(page, "frame", "turbo:frame-load") + await page.click("#outer-frame-link") + await nextEventOnTarget(page, "frame", "turbo:frame-load") + }) +}) + +test("test following a link preserves the current element's attributes", async ({ page }) => { + const currentPath = pathname(page.url()) + + await page.click("#hello a") + await nextBeat() + + const frame = await page.locator("turbo-frame#frame") + assert.equal(await frame.getAttribute("data-loaded-from"), currentPath) + assert.equal(await frame.getAttribute("src"), await propertyForSelector(page, "#hello a", "href")) +}) + +test("test following a link sets the frame element's [src]", async ({ page }) => { + await page.click("#link-frame-with-search-params") + + const { url } = await nextEventOnTarget(page, "frame", "turbo:before-fetch-request") + const fetchRequestUrl = new URL(url) + + assert.equal(fetchRequestUrl.pathname, "/src/tests/fixtures/frames/frame.html") + assert.equal(fetchRequestUrl.searchParams.get("key"), "value", "fetch request encodes query parameters") + + await nextBeat() + const src = new URL((await attributeForSelector(page, "#frame", "src")) || "") + + assert.equal(src.pathname, "/src/tests/fixtures/frames/frame.html") + assert.equal(src.searchParams.get("key"), "value", "[src] attribute encodes query parameters") +}) + +test("test a frame whose src references itself does not infinitely loop", async ({ page }) => { + await page.click("#frame-self") + + await nextEventOnTarget(page, "frame", "turbo:frame-render") + await nextEventOnTarget(page, "frame", "turbo:frame-load") + + const otherEvents = await readEventLogs(page) + assert.equal(otherEvents.length, 0, "no more events") +}) + +test("test following a link driving a frame toggles the [aria-busy=true] attribute", async ({ page }) => { + await page.click("#hello a") + + assert.equal(await nextAttributeMutationNamed(page, "frame", "busy"), "", "sets [busy] on the #frame") + assert.equal( + await nextAttributeMutationNamed(page, "frame", "aria-busy"), + "true", + "sets [aria-busy=true] on the #frame" + ) + assert.equal(await nextAttributeMutationNamed(page, "frame", "busy"), null, "removes [busy] on the #frame") + assert.equal( + await nextAttributeMutationNamed(page, "frame", "aria-busy"), + null, + "removes [aria-busy] from the #frame" + ) +}) + +test("test following a link to a page without a matching frame results in an empty frame", async ({ page }) => { + await page.click("#missing a") + await nextBeat() + assert.notOk(await innerHTMLForSelector(page, "#missing")) +}) + +test("test following a link within a frame with a target set navigates the target frame", async ({ page }) => { + await page.click("#hello a") + await nextBeat() + + const frameText = await page.textContent("#frame h2") + assert.equal(frameText, "Frame: Loaded") +}) + +test("test following a link in rapid succession cancels the previous request", async ({ page }) => { + await page.click("#outside-frame-form") + await page.click("#outer-frame-link") + await nextBeat() + + const frameText = await page.textContent("#frame h2") + assert.equal(frameText, "Frame: Loaded") +}) + +test("test following a link within a descendant frame whose ancestor declares a target set navigates the descendant frame", async ({ + page, +}) => { + const selector = "#nested-root[target=frame] #nested-child a:not([data-turbo-frame])" + const link = await page.locator(selector) + const href = await propertyForSelector(page, selector, "href") + + await link.click() + await nextBeat() + + const frame = await page.textContent("#frame h2") + const nestedRoot = await page.textContent("#nested-root h2") + const nestedChild = await page.textContent("#nested-child") + assert.equal(frame, "Frames: #frame") + assert.equal(nestedRoot, "Frames: #nested-root") + assert.equal(nestedChild, "Frame: Loaded") + assert.equal(await attributeForSelector(page, "#frame", "src"), null) + assert.equal(await attributeForSelector(page, "#nested-root", "src"), null) + assert.equal(await attributeForSelector(page, "#nested-child", "src"), href || "") +}) + +test("test following a link that declares data-turbo-frame within a frame whose ancestor respects the override", async ({ + page, +}) => { + await page.click("#nested-root[target=frame] #nested-child a[data-turbo-frame]") + await nextBeat() + + const frameText = await page.textContent("body > h1") + assert.equal(frameText, "One") + assert.notOk(await hasSelector(page, "#frame")) + assert.notOk(await hasSelector(page, "#nested-root")) + assert.notOk(await hasSelector(page, "#nested-child")) +}) + +test("test following a form within a nested frame with form target top", async ({ page }) => { + await page.click("#nested-child-navigate-form-top-submit") + await nextBeat() + + const frameText = await page.textContent("body > h1") + assert.equal(frameText, "One") + assert.notOk(await hasSelector(page, "#frame")) + assert.notOk(await hasSelector(page, "#nested-root")) + assert.notOk(await hasSelector(page, "#nested-child")) +}) + +test("test following a form within a nested frame with child frame target top", async ({ page }) => { + await page.click("#nested-child-navigate-top-submit") + await nextBeat() + + const frameText = await page.textContent("body > h1") + assert.equal(frameText, "One") + assert.notOk(await hasSelector(page, "#frame")) + assert.notOk(await hasSelector(page, "#nested-root")) + assert.notOk(await hasSelector(page, "#nested-child-navigate-top")) +}) + +test("test following a link within a frame with target=_top navigates the page", async ({ page }) => { + assert.equal(await attributeForSelector(page, "#navigate-top", "src"), null) + + await page.click("#navigate-top a:not([data-turbo-frame])") + await nextBeat() + + const frameText = await page.textContent("body > h1") + assert.equal(frameText, "One") + assert.notOk(await hasSelector(page, "#navigate-top a")) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await searchParams(page.url()).get("key"), "value") +}) + +test("test following a link that declares data-turbo-frame='_self' within a frame with target=_top navigates the frame itself", async ({ + page, +}) => { + assert.equal(await attributeForSelector(page, "#navigate-top", "src"), null) + + await page.click("#navigate-top a[data-turbo-frame='_self']") + await nextBeat() + + const title = await page.textContent("body > h1") + assert.equal(title, "Frames") + assert.ok(await hasSelector(page, "#navigate-top")) + const frame = await page.textContent("#navigate-top") + assert.equal(frame, "Replaced only the frame") +}) + +test("test following a link to a page with a which lazily loads a matching frame", async ({ + page, +}) => { + await nextBeat() + await page.click("#recursive summary") + assert.ok(await hasSelector(page, "#recursive details[open]")) + + await page.click("#recursive a") + await nextBeat() + assert.ok(await hasSelector(page, "#recursive details:not([open])")) +}) + +test("test submitting a form that redirects to a page with a which lazily loads a matching frame", async ({ + page, +}) => { + await nextBeat() + await page.click("#recursive summary") + assert.ok(await hasSelector(page, "#recursive details[open]")) + + await page.click("#recursive input[type=submit]") + await nextBeat() + assert.ok(await hasSelector(page, "#recursive details:not([open])")) +}) + +test("test removing [disabled] attribute from eager-loaded frame navigates it", async ({ page }) => { + await page.evaluate(() => document.getElementById("frame")?.setAttribute("disabled", "")) + await page.evaluate(() => + document.getElementById("frame")?.setAttribute("src", "/src/tests/fixtures/frames/frame.html") + ) + + assert.ok( + await noNextEventOnTarget(page, "frame", "turbo:before-fetch-request"), + "[disabled] frames do not submit requests" + ) + + await page.evaluate(() => document.getElementById("frame")?.removeAttribute("disabled")) + + await nextEventOnTarget(page, "frame", "turbo:before-fetch-request") +}) + +test("test evaluates frame script elements on each render", async ({ page }) => { + assert.equal(await frameScriptEvaluationCount(page), undefined) + + await page.click("#body-script-link") + assert.equal(await frameScriptEvaluationCount(page), 1) + + await page.click("#body-script-link") + assert.equal(await frameScriptEvaluationCount(page), 2) +}) + +test("test does not evaluate data-turbo-eval=false scripts", async ({ page }) => { + await page.click("#eval-false-script-link") + await nextBeat() + assert.equal(await frameScriptEvaluationCount(page), undefined) +}) + +test("test redirecting in a form is still navigatable after redirect", async ({ page }) => { + await nextBeat() + await page.click("#navigate-form-redirect") + await nextBeat() + assert.ok(await hasSelector(page, "#form-redirect")) + + await nextBeat() + await page.click("#submit-form") + await nextBeat() + assert.ok(await hasSelector(page, "#form-redirected-header")) + + await nextBeat() + await page.click("#navigate-form-redirect") + await nextBeat() + assert.ok(await hasSelector(page, "#form-redirect-header")) +}) + +test("test 'turbo:frame-render' is triggered after frame has finished rendering", async ({ page }) => { + await page.click("#frame-part") + + await nextEventNamed(page, "turbo:frame-render") // recursive + const { fetchResponse } = await nextEventNamed(page, "turbo:frame-render") + + assert.include(fetchResponse.response.url, "/src/tests/fixtures/frames/part.html") +}) + +test("test navigating a frame fires events", async ({ page }) => { + await page.click("#outside-frame-form") + + const { fetchResponse } = await nextEventOnTarget(page, "frame", "turbo:frame-render") + assert.include(fetchResponse.response.url, "/src/tests/fixtures/frames/form.html") + + await nextEventOnTarget(page, "frame", "turbo:frame-load") + + const otherEvents = await readEventLogs(page) + assert.equal(otherEvents.length, 0, "no more events") +}) + +test("test following inner link reloads frame on every click", async ({ page }) => { + await page.click("#hello a") + await nextEventNamed(page, "turbo:before-fetch-request") + + await page.click("#hello a") + await nextEventNamed(page, "turbo:before-fetch-request") +}) + +test("test following outer link reloads frame on every click", async ({ page }) => { + await page.click("#outer-frame-link") + await nextEventNamed(page, "turbo:before-fetch-request") + + await page.click("#outer-frame-link") + await nextEventNamed(page, "turbo:before-fetch-request") +}) + +test("test following outer form reloads frame on every submit", async ({ page }) => { + await page.click("#outer-frame-submit") + await nextEventNamed(page, "turbo:before-fetch-request") + + await page.click("#outer-frame-submit") + await nextEventNamed(page, "turbo:before-fetch-request") +}) - async "test an inner/outer link reloads frame on every click"() { - await this.clickSelector("#inner-outer-frame-link") - await this.nextEventNamed("turbo:before-fetch-request") +test("test an inner/outer link reloads frame on every click", async ({ page }) => { + await page.click("#inner-outer-frame-link") + await nextEventNamed(page, "turbo:before-fetch-request") - await this.clickSelector("#inner-outer-frame-link") - await this.nextEventNamed("turbo:before-fetch-request") - } + await page.click("#inner-outer-frame-link") + await nextEventNamed(page, "turbo:before-fetch-request") +}) - async "test an inner/outer form reloads frame on every submit"() { - await this.clickSelector("#inner-outer-frame-submit") - await this.nextEventNamed("turbo:before-fetch-request") - - await this.clickSelector("#inner-outer-frame-submit") - await this.nextEventNamed("turbo:before-fetch-request") - } +test("test an inner/outer form reloads frame on every submit", async ({ page }) => { + await page.click("#inner-outer-frame-submit") + await nextEventNamed(page, "turbo:before-fetch-request") - async "test reconnecting after following a link does not reload the frame"() { - await this.clickSelector("#hello a") - await this.nextEventNamed("turbo:before-fetch-request") + await page.click("#inner-outer-frame-submit") + await nextEventNamed(page, "turbo:before-fetch-request") +}) - await this.remote.execute(() => { - window.savedElement = document.querySelector("#frame") - window.savedElement?.remove() - }) - await this.nextBeat +test("test reconnecting after following a link does not reload the frame", async ({ page }) => { + await page.click("#hello a") + await nextEventNamed(page, "turbo:before-fetch-request") - await this.remote.execute(() => { - if (window.savedElement) { - document.body.appendChild(window.savedElement) + await page.evaluate(() => { + window.savedElement = document.querySelector("#frame") + window.savedElement?.remove() + }) + await nextBeat() + + await page.evaluate(() => { + if (window.savedElement) { + document.body.appendChild(window.savedElement) + } + }) + await nextBeat() + + const eventLogs = await readEventLogs(page) + const requestLogs = eventLogs.filter(([name]) => name == "turbo:before-fetch-request") + assert.equal(requestLogs.length, 0) +}) + +test("test navigating pushing URL state from a frame navigation fires events", async ({ page }) => { + await page.click("#link-outside-frame-action-advance") + + assert.equal( + await nextAttributeMutationNamed(page, "frame", "aria-busy"), + "true", + "sets aria-busy on the " + ) + await nextEventOnTarget(page, "frame", "turbo:before-fetch-request") + await nextEventOnTarget(page, "frame", "turbo:before-fetch-response") + await nextEventOnTarget(page, "frame", "turbo:frame-render") + await nextEventOnTarget(page, "frame", "turbo:frame-load") + assert.notOk(await nextAttributeMutationNamed(page, "frame", "aria-busy"), "removes aria-busy from the ") + + assert.equal(await nextAttributeMutationNamed(page, "html", "aria-busy"), "true", "sets aria-busy on the ") + await nextEventOnTarget(page, "html", "turbo:before-visit") + await nextEventOnTarget(page, "html", "turbo:visit") + await nextEventOnTarget(page, "html", "turbo:before-cache") + await nextEventOnTarget(page, "html", "turbo:before-render") + await nextEventOnTarget(page, "html", "turbo:render") + await nextEventOnTarget(page, "html", "turbo:load") + assert.notOk(await nextAttributeMutationNamed(page, "html", "aria-busy"), "removes aria-busy from the ") +}) + +test("test navigating a frame with a form[method=get] that does not redirect still updates the [src]", async ({ + page, +}) => { + await page.click("#frame-form-get-no-redirect") + await nextEventNamed(page, "turbo:before-fetch-request") + await nextEventNamed(page, "turbo:before-fetch-response") + await nextEventOnTarget(page, "frame", "turbo:frame-render") + await nextEventOnTarget(page, "frame", "turbo:frame-load") + await noNextEventNamed(page, "turbo:before-fetch-request") + + const src = (await attributeForSelector(page, "#frame", "src")) ?? "" + + assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") + assert.equal(await page.textContent("h1"), "Frames") + assert.equal(await page.textContent("#frame h2"), "Frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames.html") +}) + +test("test navigating turbo-frame[data-turbo-action=advance] from within pushes URL state", async ({ page }) => { + await page.click("#add-turbo-action-to-frame") + await page.click("#link-frame") + await nextEventNamed(page, "turbo:load") + + const title = await page.textContent("h1") + const frameTitle = await page.textContent("#frame h2") + + assert.equal(title, "Frames") + assert.equal(frameTitle, "Frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/frame.html") +}) + +test("test navigating turbo-frame[data-turbo-action=advance] to the same URL clears the [aria-busy] and [data-turbo-preview] state", async ({ + page, +}) => { + await page.click("#link-outside-frame-action-advance") + await nextEventNamed(page, "turbo:load") + await page.click("#link-outside-frame-action-advance") + await nextEventNamed(page, "turbo:load") + await page.click("#link-outside-frame-action-advance") + await nextEventNamed(page, "turbo:load") + + assert.equal(await attributeForSelector(page, "#frame", "aria-busy"), null, "clears turbo-frame[aria-busy]") + assert.equal(await attributeForSelector(page, "#html", "aria-busy"), null, "clears html[aria-busy]") + assert.equal(await attributeForSelector(page, "#html", "data-turbo-preview"), null, "clears html[aria-busy]") +}) + +test("test navigating a turbo-frame with an a[data-turbo-action=advance] preserves page state", async ({ page }) => { + await scrollToSelector(page, "#below-the-fold-input") + await page.fill("#below-the-fold-input", "a value") + await page.click("#below-the-fold-link-frame-action") + await nextEventNamed(page, "turbo:load") + + const title = await page.textContent("h1") + const frameTitle = await page.textContent("#frame h2") + const src = (await attributeForSelector(page, "#frame", "src")) ?? "" + + assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") + assert.equal(title, "Frames") + assert.equal(frameTitle, "Frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/frame.html") + assert.equal(await propertyForSelector(page, "#below-the-fold-input", "value"), "a value", "preserves page state") + + const { y } = await scrollPosition(page) + assert.notEqual(y, 0, "preserves Y scroll position") +}) + +test("test a turbo-frame that has been driven by a[data-turbo-action] can be navigated normally", async ({ page }) => { + await page.click("#remove-target-from-hello") + await page.click("#link-hello-advance") + await nextEventNamed(page, "turbo:load") + + assert.equal(await page.textContent("h1"), "Frames") + assert.equal(await page.textContent("#hello h2"), "Hello from a frame") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/hello.html") + + await page.click("#hello a") + await nextEventOnTarget(page, "hello", "turbo:frame-load") + await noNextEventNamed(page, "turbo:load") + + assert.equal(await page.textContent("#hello h2"), "Frames: #hello") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/hello.html") +}) + +test("test navigating turbo-frame from within with a[data-turbo-action=advance] pushes URL state", async ({ page }) => { + await page.click("#link-nested-frame-action-advance") + await nextEventNamed(page, "turbo:load") + + const title = await page.textContent("h1") + const frameTitle = await page.textContent("#frame h2") + const src = (await attributeForSelector(page, "#frame", "src")) ?? "" + + assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") + assert.equal(title, "Frames") + assert.equal(frameTitle, "Frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/frame.html") + assert.ok(await hasSelector(page, "#frame[complete]"), "marks the frame as [complete]") +}) + +test("test navigating frame with a[data-turbo-action=advance] pushes URL state", async ({ page }) => { + await page.click("#link-outside-frame-action-advance") + await nextEventNamed(page, "turbo:load") + + const title = await page.textContent("h1") + const frameTitle = await page.textContent("#frame h2") + const src = (await attributeForSelector(page, "#frame", "src")) ?? "" + + assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") + assert.equal(title, "Frames") + assert.equal(frameTitle, "Frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/frame.html") + assert.ok(await hasSelector(page, "#frame[complete]"), "marks the frame as [complete]") +}) + +test("test navigating frame with form[method=get][data-turbo-action=advance] pushes URL state", async ({ page }) => { + await page.click("#form-get-frame-action-advance button") + await nextEventNamed(page, "turbo:load") + + const title = await page.textContent("h1") + const frameTitle = await page.textContent("#frame h2") + const src = (await attributeForSelector(page, "#frame", "src")) ?? "" + + assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") + assert.equal(title, "Frames") + assert.equal(frameTitle, "Frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/frame.html") + assert.ok(await hasSelector(page, "#frame[complete]"), "marks the frame as [complete]") +}) + +test("test navigating frame with form[method=get][data-turbo-action=advance] to the same URL clears the [aria-busy] and [data-turbo-preview] state", async ({ + page, +}) => { + await page.click("#form-get-frame-action-advance button") + await nextEventNamed(page, "turbo:load") + await page.click("#form-get-frame-action-advance button") + await nextEventNamed(page, "turbo:load") + await page.click("#form-get-frame-action-advance button") + await nextEventNamed(page, "turbo:load") + + assert.equal(await attributeForSelector(page, "#frame", "aria-busy"), null, "clears turbo-frame[aria-busy]") + assert.equal(await attributeForSelector(page, "#html", "aria-busy"), null, "clears html[aria-busy]") + assert.equal(await attributeForSelector(page, "#html", "data-turbo-preview"), null, "clears html[aria-busy]") +}) + +test("test navigating frame with form[method=post][data-turbo-action=advance] pushes URL state", async ({ page }) => { + await page.click("#form-post-frame-action-advance button") + await nextEventNamed(page, "turbo:load") + + const title = await page.textContent("h1") + const frameTitle = await page.textContent("#frame h2") + const src = (await attributeForSelector(page, "#frame", "src")) ?? "" + + assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") + assert.equal(title, "Frames") + assert.equal(frameTitle, "Frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/frame.html") + assert.ok(await hasSelector(page, "#frame[complete]"), "marks the frame as [complete]") +}) + +test("test navigating frame with form[method=post][data-turbo-action=advance] to the same URL clears the [aria-busy] and [data-turbo-preview] state", async ({ + page, +}) => { + await page.click("#form-post-frame-action-advance button") + await nextEventNamed(page, "turbo:load") + await page.click("#form-post-frame-action-advance button") + await nextEventNamed(page, "turbo:load") + await page.click("#form-post-frame-action-advance button") + await nextEventNamed(page, "turbo:load") + + assert.equal(await attributeForSelector(page, "#frame", "aria-busy"), null, "clears turbo-frame[aria-busy]") + assert.equal(await attributeForSelector(page, "#html", "aria-busy"), null, "clears html[aria-busy]") + assert.equal(await attributeForSelector(page, "#html", "data-turbo-preview"), null, "clears html[aria-busy]") + assert.ok(await hasSelector(page, "#frame[complete]"), "marks the frame as [complete]") +}) + +test("test navigating frame with button[data-turbo-action=advance] pushes URL state", async ({ page }) => { + await page.click("#button-frame-action-advance") + await nextEventNamed(page, "turbo:load") + + const title = await page.textContent("h1") + const frameTitle = await page.textContent("#frame h2") + const src = (await attributeForSelector(page, "#frame", "src")) ?? "" + + assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") + assert.equal(title, "Frames") + assert.equal(frameTitle, "Frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/frame.html") + assert.ok(await hasSelector(page, "#frame[complete]"), "marks the frame as [complete]") +}) + +test("test navigating back after pushing URL state from a turbo-frame[data-turbo-action=advance] restores the frames previous contents", async ({ + page, +}) => { + await page.click("#add-turbo-action-to-frame") + await page.click("#link-frame") + await nextEventNamed(page, "turbo:load") + await page.goBack() + await nextEventNamed(page, "turbo:load") + + const title = await page.textContent("h1") + const frameTitle = await page.textContent("#frame h2") + + assert.equal(title, "Frames") + assert.equal(frameTitle, "Frames: #frame") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames.html") + assert.equal(await propertyForSelector(page, "#frame", "src"), null) +}) + +test("test navigating back then forward after pushing URL state from a turbo-frame[data-turbo-action=advance] restores the frames next contents", async ({ + page, +}) => { + await page.click("#add-turbo-action-to-frame") + await page.click("#link-frame") + await nextEventNamed(page, "turbo:load") + await page.goBack() + await nextEventNamed(page, "turbo:load") + await page.goForward() + await nextEventNamed(page, "turbo:load") + + const title = await page.textContent("h1") + const frameTitle = await page.textContent("#frame h2") + const src = (await attributeForSelector(page, "#frame", "src")) ?? "" + + assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") + assert.equal(title, "Frames") + assert.equal(frameTitle, "Frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/frame.html") + assert.ok(await hasSelector(page, "#frame[complete]"), "marks the frame as [complete]") +}) + +test("test turbo:before-fetch-request fires on the frame element", async ({ page }) => { + await page.click("#hello a") + assert.ok(await nextEventOnTarget(page, "frame", "turbo:before-fetch-request")) +}) + +test("test turbo:before-fetch-response fires on the frame element", async ({ page }) => { + await page.click("#hello a") + assert.ok(await nextEventOnTarget(page, "frame", "turbo:before-fetch-response")) +}) + +test("test navigating a eager frame with a link[method=get] that does not fetch eager frame twice", async ({ + page, +}) => { + await page.click("#link-to-eager-loaded-frame") + + await nextBeat() + + const eventLogs = await readEventLogs(page) + const fetchLogs = eventLogs.filter( + ([name, options]) => + name == "turbo:before-fetch-request" && options?.url?.includes("/src/tests/fixtures/frames/frame_for_eager.html") + ) + assert.equal(fetchLogs.length, 1) + + const src = (await attributeForSelector(page, "#eager-loaded-frame", "src")) ?? "" + assert.ok(src.includes("/src/tests/fixtures/frames/frame_for_eager.html"), "updates src attribute") + assert.equal(await page.textContent("h1"), "Eager-loaded frame") + assert.equal(await page.textContent("#eager-loaded-frame h2"), "Eager-loaded frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/page_with_eager_frame.html") +}) + +async function withoutChangingEventListenersCount(page: Page, callback: () => Promise) { + const name = "eventListenersAttachedToDocument" + const setup = () => { + return page.evaluate((name) => { + const context = window as any + context[name] = 0 + context.originals = { + addEventListener: document.addEventListener, + removeEventListener: document.removeEventListener, } - }) - await this.nextBeat - - const eventLogs = await this.eventLogChannel.read() - const requestLogs = eventLogs.filter(([name]) => name == "turbo:before-fetch-request") - this.assert.equal(requestLogs.length, 0) - } - - async "test navigating pushing URL state from a frame navigation fires events"() { - await this.clickSelector("#link-outside-frame-action-advance") - - this.assert.equal( - await this.nextAttributeMutationNamed("frame", "aria-busy"), - "true", - "sets aria-busy on the " - ) - await this.nextEventOnTarget("frame", "turbo:before-fetch-request") - await this.nextEventOnTarget("frame", "turbo:before-fetch-response") - await this.nextEventOnTarget("frame", "turbo:frame-render") - await this.nextEventOnTarget("frame", "turbo:frame-load") - this.assert.notOk( - await this.nextAttributeMutationNamed("frame", "aria-busy"), - "removes aria-busy from the " - ) - - this.assert.equal( - await this.nextAttributeMutationNamed("html", "aria-busy"), - "true", - "sets aria-busy on the " - ) - await this.nextEventOnTarget("html", "turbo:before-visit") - await this.nextEventOnTarget("html", "turbo:visit") - await this.nextEventOnTarget("html", "turbo:before-cache") - await this.nextEventOnTarget("html", "turbo:before-render") - await this.nextEventOnTarget("html", "turbo:render") - await this.nextEventOnTarget("html", "turbo:load") - this.assert.notOk(await this.nextAttributeMutationNamed("html", "aria-busy"), "removes aria-busy from the ") - } - - async "test navigating a frame with a form[method=get] that does not redirect still updates the [src]"() { - await this.clickSelector("#frame-form-get-no-redirect") - await this.nextEventNamed("turbo:before-fetch-request") - await this.nextEventNamed("turbo:before-fetch-response") - await this.nextEventOnTarget("frame", "turbo:frame-render") - await this.nextEventOnTarget("frame", "turbo:frame-load") - await this.noNextEventNamed("turbo:before-fetch-request") - - const src = (await this.attributeForSelector("#frame", "src")) ?? "" - - this.assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") - this.assert.equal(await (await this.querySelector("h1")).getVisibleText(), "Frames") - this.assert.equal(await (await this.querySelector("#frame h2")).getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames.html") - } - - async "test navigating turbo-frame[data-turbo-action=advance] from within pushes URL state"() { - await this.clickSelector("#add-turbo-action-to-frame") - await this.clickSelector("#link-frame") - await this.nextEventNamed("turbo:load") - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#frame h2") - - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") - } - - async "test navigating turbo-frame[data-turbo-action=advance] to the same URL clears the [aria-busy] and [data-turbo-preview] state"() { - await this.clickSelector("#link-outside-frame-action-advance") - await this.nextEventNamed("turbo:load") - await this.clickSelector("#link-outside-frame-action-advance") - await this.nextEventNamed("turbo:load") - await this.clickSelector("#link-outside-frame-action-advance") - await this.nextEventNamed("turbo:load") - - this.assert.equal(await this.attributeForSelector("#frame", "aria-busy"), null, "clears turbo-frame[aria-busy]") - this.assert.equal(await this.attributeForSelector("#html", "aria-busy"), null, "clears html[aria-busy]") - this.assert.equal(await this.attributeForSelector("#html", "data-turbo-preview"), null, "clears html[aria-busy]") - } - - async "test navigating a turbo-frame with an a[data-turbo-action=advance] preserves page state"() { - await this.scrollToSelector("#below-the-fold-input") - await this.fillInSelector("#below-the-fold-input", "a value") - await this.clickSelector("#below-the-fold-link-frame-action") - await this.nextEventNamed("turbo:load") - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#frame h2") - const src = (await this.attributeForSelector("#frame", "src")) ?? "" - - this.assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") - this.assert.equal( - await this.propertyForSelector("#below-the-fold-input", "value"), - "a value", - "preserves page state" - ) - - const { y } = await this.scrollPosition - this.assert.notEqual(y, 0, "preserves Y scroll position") - } - - async "test a turbo-frame that has been driven by a[data-turbo-action] can be navigated normally"() { - await this.clickSelector("#remove-target-from-hello") - await this.clickSelector("#link-hello-advance") - await this.nextEventNamed("turbo:load") - - this.assert.equal(await (await this.querySelector("h1")).getVisibleText(), "Frames") - this.assert.equal(await (await this.querySelector("#hello h2")).getVisibleText(), "Hello from a frame") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/hello.html") - - await this.clickSelector("#hello a") - await this.nextEventOnTarget("hello", "turbo:frame-load") - await this.noNextEventNamed("turbo:load") - - this.assert.equal(await (await this.querySelector("#hello h2")).getVisibleText(), "Frames: #hello") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/hello.html") - } - - async "test navigating turbo-frame from within with a[data-turbo-action=advance] pushes URL state"() { - await this.clickSelector("#link-nested-frame-action-advance") - await this.nextEventNamed("turbo:load") - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#frame h2") - const src = (await this.attributeForSelector("#frame", "src")) ?? "" - - this.assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") - this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") - } - - async "test navigating frame with a[data-turbo-action=advance] pushes URL state"() { - await this.clickSelector("#link-outside-frame-action-advance") - await this.nextEventNamed("turbo:load") - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#frame h2") - const src = (await this.attributeForSelector("#frame", "src")) ?? "" - this.assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") - this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") - } - - async "test navigating frame with form[method=get][data-turbo-action=advance] pushes URL state"() { - await this.clickSelector("#form-get-frame-action-advance button") - await this.nextEventNamed("turbo:load") - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#frame h2") - const src = (await this.attributeForSelector("#frame", "src")) ?? "" - - this.assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") - this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") - } - - async "test navigating frame with form[method=get][data-turbo-action=advance] to the same URL clears the [aria-busy] and [data-turbo-preview] state"() { - await this.clickSelector("#form-get-frame-action-advance button") - await this.nextEventNamed("turbo:load") - await this.clickSelector("#form-get-frame-action-advance button") - await this.nextEventNamed("turbo:load") - await this.clickSelector("#form-get-frame-action-advance button") - await this.nextEventNamed("turbo:load") - - this.assert.equal(await this.attributeForSelector("#frame", "aria-busy"), null, "clears turbo-frame[aria-busy]") - this.assert.equal(await this.attributeForSelector("#html", "aria-busy"), null, "clears html[aria-busy]") - this.assert.equal(await this.attributeForSelector("#html", "data-turbo-preview"), null, "clears html[aria-busy]") - } - - async "test navigating frame with form[method=post][data-turbo-action=advance] pushes URL state"() { - await this.clickSelector("#form-post-frame-action-advance button") - await this.nextEventNamed("turbo:load") - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#frame h2") - const src = (await this.attributeForSelector("#frame", "src")) ?? "" - - this.assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") - this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") - } - - async "test navigating frame with form[method=post][data-turbo-action=advance] to the same URL clears the [aria-busy] and [data-turbo-preview] state"() { - await this.clickSelector("#form-post-frame-action-advance button") - await this.nextEventNamed("turbo:load") - await this.clickSelector("#form-post-frame-action-advance button") - await this.nextEventNamed("turbo:load") - await this.clickSelector("#form-post-frame-action-advance button") - await this.nextEventNamed("turbo:load") - - this.assert.equal(await this.attributeForSelector("#frame", "aria-busy"), null, "clears turbo-frame[aria-busy]") - this.assert.equal(await this.attributeForSelector("#html", "aria-busy"), null, "clears html[aria-busy]") - this.assert.equal(await this.attributeForSelector("#html", "data-turbo-preview"), null, "clears html[aria-busy]") - this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") - } - - async "test navigating frame with button[data-turbo-action=advance] pushes URL state"() { - await this.clickSelector("#button-frame-action-advance") - await this.nextEventNamed("turbo:load") - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#frame h2") - const src = (await this.attributeForSelector("#frame", "src")) ?? "" - - this.assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") - this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") - } - - async "test navigating back after pushing URL state from a turbo-frame[data-turbo-action=advance] restores the frames previous contents"() { - await this.clickSelector("#add-turbo-action-to-frame") - await this.clickSelector("#link-frame") - await this.nextEventNamed("turbo:load") - await this.goBack() - await this.nextEventNamed("turbo:load") - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#frame h2") + document.addEventListener = ( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ) => { + context.originals.addEventListener.call(document, type, listener, options) + context[name] += 1 + } - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.equal(await frameTitle.getVisibleText(), "Frames: #frame") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames.html") - this.assert.equal(await this.propertyForSelector("#frame", "src"), null) - } + document.removeEventListener = ( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ) => { + context.originals.removeEventListener.call(document, type, listener, options) + context[name] -= 1 + } - async "test navigating back then forward after pushing URL state from a turbo-frame[data-turbo-action=advance] restores the frames next contents"() { - await this.clickSelector("#add-turbo-action-to-frame") - await this.clickSelector("#link-frame") - await this.nextEventNamed("turbo:load") - await this.goBack() - await this.nextEventNamed("turbo:load") - await this.goForward() - await this.nextEventNamed("turbo:load") - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#frame h2") - const src = (await this.attributeForSelector("#frame", "src")) ?? "" - - this.assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") - this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") + return context[name] || 0 + }, name) } - async "test turbo:before-fetch-request fires on the frame element"() { - await this.clickSelector("#hello a") - this.assert.ok(await this.nextEventOnTarget("frame", "turbo:before-fetch-request")) - } + const teardown = () => { + return page.evaluate((name) => { + const context = window as any + const { addEventListener, removeEventListener } = context.originals - async "test turbo:before-fetch-response fires on the frame element"() { - await this.clickSelector("#hello a") - this.assert.ok(await this.nextEventOnTarget("frame", "turbo:before-fetch-response")) - } + document.addEventListener = addEventListener + document.removeEventListener = removeEventListener - async "test navigating a eager frame with a link[method=get] that does not fetch eager frame twice"() { - await this.clickSelector("#link-to-eager-loaded-frame") - - await this.nextBeat - - const eventLogs = await this.eventLogChannel.read() - const fetchLogs = eventLogs.filter( - ([name, options]) => - name == "turbo:before-fetch-request" && - options?.url?.includes("/src/tests/fixtures/frames/frame_for_eager.html") - ) - this.assert.equal(fetchLogs.length, 1) - - const src = (await this.attributeForSelector("#eager-loaded-frame", "src")) ?? "" - this.assert.ok(src.includes("/src/tests/fixtures/frames/frame_for_eager.html"), "updates src attribute") - this.assert.equal(await (await this.querySelector("h1")).getVisibleText(), "Eager-loaded frame") - this.assert.equal( - await (await this.querySelector("#eager-loaded-frame h2")).getVisibleText(), - "Eager-loaded frame: Loaded" - ) - this.assert.equal(await this.pathname, "/src/tests/fixtures/page_with_eager_frame.html") + return context[name] || 0 + }, name) } - async withoutChangingEventListenersCount(callback: () => void) { - const name = "eventListenersAttachedToDocument" - const setup = () => { - return this.evaluate( - (name: string) => { - const context = window as any - context[name] = 0 - context.originals = { - addEventListener: document.addEventListener, - removeEventListener: document.removeEventListener, - } - - document.addEventListener = ( - type: string, - listener: EventListenerOrEventListenerObject, - options?: boolean | AddEventListenerOptions - ) => { - context.originals.addEventListener.call(document, type, listener, options) - context[name] += 1 - } - - document.removeEventListener = ( - type: string, - listener: EventListenerOrEventListenerObject, - options?: boolean | AddEventListenerOptions - ) => { - context.originals.removeEventListener.call(document, type, listener, options) - context[name] -= 1 - } - - return context[name] || 0 - }, - [name] - ) - } - - const teardown = () => { - return this.evaluate( - (name: string) => { - const context = window as any - const { addEventListener, removeEventListener } = context.originals - - document.addEventListener = addEventListener - document.removeEventListener = removeEventListener - - return context[name] || 0 - }, - [name] - ) - } + const originalCount = await setup() + await callback() + const finalCount = await teardown() - const originalCount = await setup() - await callback() - const finalCount = await teardown() - - this.assert.equal(finalCount, originalCount, "expected callback not to leak event listeners") - } - - async fillInSelector(selector: string, value: string) { - const element = await this.querySelector(selector) - - await element.click() - - return element.type(value) - } + assert.equal(finalCount, originalCount, "expected callback not to leak event listeners") +} - get frameScriptEvaluationCount(): Promise { - return this.evaluate(() => window.frameScriptEvaluationCount) - } +function frameScriptEvaluationCount(page: Page): Promise { + return page.evaluate(() => window.frameScriptEvaluationCount) } declare global { @@ -685,5 +705,3 @@ declare global { frameScriptEvaluationCount?: number } } - -FrameTests.registerSuite() diff --git a/src/tests/functional/import_tests.ts b/src/tests/functional/import_tests.ts index ede73d2f8..e2cb745a0 100644 --- a/src/tests/functional/import_tests.ts +++ b/src/tests/functional/import_tests.ts @@ -1,13 +1,10 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" -export class ImportTests extends TurboDriveTestCase { - async "test window variable with ESM"() { - await this.goToLocation("/src/tests/fixtures/esm.html") - const type = await this.evaluate(() => { - return typeof window.Turbo - }) - this.assert.equal(type, "object") - } -} - -ImportTests.registerSuite() +test("test window variable with ESM", async ({ page }) => { + await page.goto("/src/tests/fixtures/esm.html") + const type = await page.evaluate(() => { + return typeof window.Turbo + }) + assert.equal(type, "object") +}) diff --git a/src/tests/functional/index.ts b/src/tests/functional/index.ts deleted file mode 100644 index 2cd8bbd4f..000000000 --- a/src/tests/functional/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -export * from "./async_script_tests" -export * from "./autofocus_tests" -export * from "./cache_observer_tests" -export * from "./drive_disabled_tests" -export * from "./drive_tests" -export * from "./form_submission_tests" -export * from "./frame_tests" -export * from "./import_tests" -export * from "./frame_navigation_tests" -export * from "./loading_tests" -export * from "./navigation_tests" -export * from "./pausable_rendering_tests" -export * from "./pausable_requests_tests" -export * from "./preloader_tests" -export * from "./rendering_tests" -export * from "./scroll_restoration_tests" -export * from "./stream_tests" -export * from "./visit_tests" diff --git a/src/tests/functional/loading_tests.ts b/src/tests/functional/loading_tests.ts index eb0c74ffc..4ea539518 100644 --- a/src/tests/functional/loading_tests.ts +++ b/src/tests/functional/loading_tests.ts @@ -1,4 +1,15 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" +import { + attributeForSelector, + hasSelector, + nextBeat, + nextBody, + nextEventNamed, + nextEventOnTarget, + noNextEventNamed, + readEventLogs, +} from "../helpers/page" declare global { interface Window { @@ -6,207 +17,199 @@ declare global { } } -export class LoadingTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/loading.html") - } - - async "test eager loading within a details element"() { - await this.nextBeat - this.assert.ok(await this.hasSelector("#loading-eager turbo-frame#frame h2")) - this.assert.ok(await this.hasSelector("#loading-eager turbo-frame[complete]"), "has [complete] attribute") - } - - async "test lazy loading within a details element"() { - await this.nextBeat - - const frameContents = "#loading-lazy turbo-frame h2" - this.assert.notOk(await this.hasSelector(frameContents)) - this.assert.ok(await this.hasSelector("#loading-lazy turbo-frame:not([complete])")) - - await this.clickSelector("#loading-lazy summary") - await this.nextBeat - - const contents = await this.querySelector(frameContents) - this.assert.equal(await contents.getVisibleText(), "Hello from a frame") - this.assert.ok(await this.hasSelector("#loading-lazy turbo-frame[complete]"), "has [complete] attribute") - } - - async "test changing loading attribute from lazy to eager loads frame"() { - const frameContents = "#loading-lazy turbo-frame h2" - await this.nextBeat - - this.assert.notOk(await this.hasSelector(frameContents)) - - await this.remote.execute(() => - document.querySelector("#loading-lazy turbo-frame")?.setAttribute("loading", "eager") - ) - await this.nextBeat - - const contents = await this.querySelector(frameContents) - await this.clickSelector("#loading-lazy summary") - this.assert.equal(await contents.getVisibleText(), "Hello from a frame") - } - - async "test navigating a visible frame with loading=lazy navigates"() { - await this.clickSelector("#loading-lazy summary") - await this.nextBeat - - const initialContents = await this.querySelector("#hello h2") - this.assert.equal(await initialContents.getVisibleText(), "Hello from a frame") - - await this.clickSelector("#hello a") - await this.nextBeat +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/loading.html") + await readEventLogs(page) +}) - const navigatedContents = await this.querySelector("#hello h2") - this.assert.equal(await navigatedContents.getVisibleText(), "Frames: #hello") - } - - async "test changing src attribute on a frame with loading=lazy defers navigation"() { - const frameContents = "#loading-lazy turbo-frame h2" - await this.nextBeat - - await this.remote.execute(() => - document.querySelector("#loading-lazy turbo-frame")?.setAttribute("src", "/src/tests/fixtures/frames.html") - ) - this.assert.notOk(await this.hasSelector(frameContents)) - - await this.clickSelector("#loading-lazy summary") - await this.nextBeat - - const contents = await this.querySelector(frameContents) - this.assert.equal(await contents.getVisibleText(), "Frames: #hello") - } - - async "test changing src attribute on a frame with loading=eager navigates"() { - const frameContents = "#loading-eager turbo-frame h2" - await this.nextBeat - - await this.remote.execute(() => - document.querySelector("#loading-eager turbo-frame")?.setAttribute("src", "/src/tests/fixtures/frames.html") - ) +test("test eager loading within a details element", async ({ page }) => { + await nextBeat() + assert.ok(await hasSelector(page, "#loading-eager turbo-frame#frame h2")) + assert.ok(await hasSelector(page, "#loading-eager turbo-frame[complete]"), "has [complete] attribute") +}) - await this.clickSelector("#loading-eager summary") - await this.nextBeat +test("test lazy loading within a details element", async ({ page }) => { + await nextBeat() - const contents = await this.querySelector(frameContents) - this.assert.equal(await contents.getVisibleText(), "Frames: #frame") - } + const frameContents = "#loading-lazy turbo-frame h2" + assert.notOk(await hasSelector(page, frameContents)) + assert.ok(await hasSelector(page, "#loading-lazy turbo-frame:not([complete])")) - async "test reloading a frame reloads the content"() { - await this.nextBeat + await page.click("#loading-lazy summary") + await nextBeat() - await this.clickSelector("#loading-eager summary") - await this.nextBeat + const contents = await page.locator(frameContents) + assert.equal(await contents.textContent(), "Hello from a frame") + assert.ok(await hasSelector(page, "#loading-lazy turbo-frame[complete]"), "has [complete] attribute") +}) - const frameContent = "#loading-eager turbo-frame#frame h2" - this.assert.ok(await this.hasSelector(frameContent)) - this.assert.ok(await this.hasSelector("#loading-eager turbo-frame[complete]"), "has [complete] attribute") +test("test changing loading attribute from lazy to eager loads frame", async ({ page }) => { + const frameContents = "#loading-lazy turbo-frame h2" + await nextBeat() - await this.remote.execute(() => (document.querySelector("#loading-eager turbo-frame") as any)?.reload()) - this.assert.ok(await this.hasSelector(frameContent)) - this.assert.ok(await this.hasSelector("#loading-eager turbo-frame:not([complete])"), "clears [complete] attribute") - } + assert.notOk(await hasSelector(page, frameContents)) - async "test navigating away from a page does not reload its frames"() { - await this.clickSelector("#one") - await this.nextBody + await page.evaluate(() => document.querySelector("#loading-lazy turbo-frame")?.setAttribute("loading", "eager")) + await nextBeat() - const eventLogs = await this.eventLogChannel.read() - const requestLogs = eventLogs.filter(([name]) => name == "turbo:before-fetch-request") - this.assert.equal(requestLogs.length, 1) - } + const contents = await page.locator(frameContents) + await page.click("#loading-lazy summary") + assert.equal(await contents.textContent(), "Hello from a frame") +}) - async "test removing the [complete] attribute of an eager frame reloads the content"() { - await this.nextEventOnTarget("frame", "turbo:frame-load") - await this.remote.execute(() => document.querySelector("#loading-eager turbo-frame")?.removeAttribute("complete")) - await this.nextEventOnTarget("frame", "turbo:frame-load") +test("test navigating a visible frame with loading=lazy navigates", async ({ page }) => { + await page.click("#loading-lazy summary") + await nextBeat() - this.assert.ok( - await this.hasSelector("#loading-eager turbo-frame[complete]"), - "sets the [complete] attribute after re-loading" - ) - } + const initialContents = await page.locator("#hello h2") + assert.equal(await initialContents.textContent(), "Hello from a frame") - async "test changing [src] attribute on a [complete] frame with loading=lazy defers navigation"() { - await this.nextEventOnTarget("frame", "turbo:frame-load") - await this.clickSelector("#loading-lazy summary") - await this.nextEventOnTarget("hello", "turbo:frame-load") + await page.click("#hello a") + await nextBeat() - this.assert.ok(await this.hasSelector("#loading-lazy turbo-frame[complete]"), "lazy frame is complete") - this.assert.equal(await (await this.querySelector("#hello h2")).getVisibleText(), "Hello from a frame") + const navigatedContents = await page.locator("#hello h2") + assert.equal(await navigatedContents.textContent(), "Frames: #hello") +}) - await this.clickSelector("#loading-lazy summary") - await this.clickSelector("#one") - await this.nextEventNamed("turbo:load") - await this.goBack() - await this.nextBody - await this.noNextEventNamed("turbo:frame-load") +test("test changing src attribute on a frame with loading=lazy defers navigation", async ({ page }) => { + const frameContents = "#loading-lazy turbo-frame h2" + await nextBeat() - let src = new URL((await this.attributeForSelector("#hello", "src")) || "") + await page.evaluate(() => + document.querySelector("#loading-lazy turbo-frame")?.setAttribute("src", "/src/tests/fixtures/frames.html") + ) + assert.notOk(await hasSelector(page, frameContents)) - this.assert.ok(await this.hasSelector("#loading-lazy turbo-frame[complete]"), "lazy frame is complete") - this.assert.equal(src.pathname, "/src/tests/fixtures/frames/hello.html", "lazy frame retains [src]") + await page.click("#loading-lazy summary") + await nextBeat() - await this.clickSelector("#link-lazy-frame") - await this.noNextEventNamed("turbo:frame-load") + const contents = await page.locator(frameContents) + assert.equal(await contents.textContent(), "Frames: #hello") +}) - this.assert.ok(await this.hasSelector("#loading-lazy turbo-frame:not([complete])"), "lazy frame is not complete") +test("test changing src attribute on a frame with loading=eager navigates", async ({ page }) => { + const frameContents = "#loading-eager turbo-frame h2" + await nextBeat() - await this.clickSelector("#loading-lazy summary") - await this.nextEventOnTarget("hello", "turbo:frame-load") + await page.evaluate(() => + document.querySelector("#loading-eager turbo-frame")?.setAttribute("src", "/src/tests/fixtures/frames.html") + ) - src = new URL((await this.attributeForSelector("#hello", "src")) || "") + await page.click("#loading-eager summary") + await nextBeat() - this.assert.equal( - await (await this.querySelector("#loading-lazy turbo-frame h2")).getVisibleText(), - "Frames: #hello" - ) - this.assert.ok(await this.hasSelector("#loading-lazy turbo-frame[complete]"), "lazy frame is complete") - this.assert.equal(src.pathname, "/src/tests/fixtures/frames.html", "lazy frame navigates") - } + const contents = await page.locator(frameContents) + assert.equal(await contents.textContent(), "Frames: #frame") +}) - async "test navigating away from a page and then back does not reload its frames"() { - await this.clickSelector("#one") - await this.nextBody - await this.eventLogChannel.read() - await this.goBack() - await this.nextBody - - const eventLogs = await this.eventLogChannel.read() - const requestLogs = eventLogs.filter(([name]) => name == "turbo:before-fetch-request") - const requestsOnEagerFrame = requestLogs.filter((record) => record[2] == "frame") - const requestsOnLazyFrame = requestLogs.filter((record) => record[2] == "hello") - - this.assert.equal(requestsOnEagerFrame.length, 0, "does not reload eager frame") - this.assert.equal(requestsOnLazyFrame.length, 0, "does not reload lazy frame") - - await this.clickSelector("#loading-lazy summary") - await this.nextEventOnTarget("hello", "turbo:before-fetch-request") - await this.nextEventOnTarget("hello", "turbo:frame-render") - await this.nextEventOnTarget("hello", "turbo:frame-load") - } +test("test reloading a frame reloads the content", async ({ page }) => { + await nextBeat() - async "test disconnecting and reconnecting a frame does not reload the frame"() { - await this.nextBeat - - await this.remote.execute(() => { - window.savedElement = document.querySelector("#loading-eager") - window.savedElement?.remove() - }) - await this.nextBeat - - await this.remote.execute(() => { - if (window.savedElement) { - document.body.appendChild(window.savedElement) - } - }) - await this.nextBeat - - const eventLogs = await this.eventLogChannel.read() - const requestLogs = eventLogs.filter(([name]) => name == "turbo:before-fetch-request") - this.assert.equal(requestLogs.length, 0) - } -} + await page.click("#loading-eager summary") + await nextBeat() + + const frameContent = "#loading-eager turbo-frame#frame h2" + assert.ok(await hasSelector(page, frameContent)) + assert.ok(await hasSelector(page, "#loading-eager turbo-frame[complete]"), "has [complete] attribute") + + await page.evaluate(() => (document.querySelector("#loading-eager turbo-frame") as any)?.reload()) + assert.ok(await hasSelector(page, frameContent)) + assert.ok(await hasSelector(page, "#loading-eager turbo-frame:not([complete])"), "clears [complete] attribute") +}) + +test("test navigating away from a page does not reload its frames", async ({ page }) => { + await page.click("#one") + await nextBody(page) + + const eventLogs = await readEventLogs(page) + const requestLogs = eventLogs.filter(([name]) => name == "turbo:before-fetch-request") + assert.equal(requestLogs.length, 1) +}) + +test("test removing the [complete] attribute of an eager frame reloads the content", async ({ page }) => { + await nextEventOnTarget(page, "frame", "turbo:frame-load") + await page.evaluate(() => document.querySelector("#loading-eager turbo-frame")?.removeAttribute("complete")) + await nextEventOnTarget(page, "frame", "turbo:frame-load") -LoadingTests.registerSuite() + assert.ok( + await hasSelector(page, "#loading-eager turbo-frame[complete]"), + "sets the [complete] attribute after re-loading" + ) +}) + +test("test changing [src] attribute on a [complete] frame with loading=lazy defers navigation", async ({ page }) => { + await nextEventOnTarget(page, "frame", "turbo:frame-load") + await page.click("#loading-lazy summary") + await nextEventOnTarget(page, "hello", "turbo:frame-load") + + assert.ok(await hasSelector(page, "#loading-lazy turbo-frame[complete]"), "lazy frame is complete") + assert.equal(await page.textContent("#hello h2"), "Hello from a frame") + + await page.click("#loading-lazy summary") + await page.click("#one") + await nextEventNamed(page, "turbo:load") + await page.goBack() + await nextBody(page) + await noNextEventNamed(page, "turbo:frame-load") + + let src = new URL((await attributeForSelector(page, "#hello", "src")) || "") + + assert.ok(await hasSelector(page, "#loading-lazy turbo-frame[complete]"), "lazy frame is complete") + assert.equal(src.pathname, "/src/tests/fixtures/frames/hello.html", "lazy frame retains [src]") + + await page.click("#link-lazy-frame") + await noNextEventNamed(page, "turbo:frame-load") + + assert.ok(await hasSelector(page, "#loading-lazy turbo-frame:not([complete])"), "lazy frame is not complete") + + await page.click("#loading-lazy summary") + await nextEventOnTarget(page, "hello", "turbo:frame-load") + + src = new URL((await attributeForSelector(page, "#hello", "src")) || "") + + assert.equal(await page.textContent("#loading-lazy turbo-frame h2"), "Frames: #hello") + assert.ok(await hasSelector(page, "#loading-lazy turbo-frame[complete]"), "lazy frame is complete") + assert.equal(src.pathname, "/src/tests/fixtures/frames.html", "lazy frame navigates") +}) + +test("test navigating away from a page and then back does not reload its frames", async ({ page }) => { + await page.click("#one") + await nextBody(page) + await readEventLogs(page) + await page.goBack() + await nextBody(page) + + const eventLogs = await readEventLogs(page) + const requestLogs = eventLogs.filter(([name]) => name == "turbo:before-fetch-request") + const requestsOnEagerFrame = requestLogs.filter((record) => record[2] == "frame") + const requestsOnLazyFrame = requestLogs.filter((record) => record[2] == "hello") + + assert.equal(requestsOnEagerFrame.length, 0, "does not reload eager frame") + assert.equal(requestsOnLazyFrame.length, 0, "does not reload lazy frame") + + await page.click("#loading-lazy summary") + await nextEventOnTarget(page, "hello", "turbo:before-fetch-request") + await nextEventOnTarget(page, "hello", "turbo:frame-render") + await nextEventOnTarget(page, "hello", "turbo:frame-load") +}) + +test("test disconnecting and reconnecting a frame does not reload the frame", async ({ page }) => { + await nextBeat() + + await page.evaluate(() => { + window.savedElement = document.querySelector("#loading-eager") + window.savedElement?.remove() + }) + await nextBeat() + + await page.evaluate(() => { + if (window.savedElement) { + document.body.appendChild(window.savedElement) + } + }) + await nextBeat() + + const eventLogs = await readEventLogs(page) + const requestLogs = eventLogs.filter(([name]) => name == "turbo:before-fetch-request") + assert.equal(requestLogs.length, 0) +}) diff --git a/src/tests/functional/navigation_tests.ts b/src/tests/functional/navigation_tests.ts index 9544c7a33..b5e97fb06 100644 --- a/src/tests/functional/navigation_tests.ts +++ b/src/tests/functional/navigation_tests.ts @@ -1,333 +1,350 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" - -export class NavigationTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/navigation.html") - } - - async "test navigating renders a progress bar"() { - const styleElement = await this.querySelector("style") - - this.assert.equal( - await styleElement.getProperty("nonce"), - "123", - "renders progress bar stylesheet inline with nonce" - ) - - await this.remote.execute(() => window.Turbo.setProgressBarDelay(0)) - await this.clickSelector("#delayed-link") - - await this.waitUntilSelector(".turbo-progress-bar") - this.assert.ok(await this.hasSelector(".turbo-progress-bar"), "displays progress bar") - - await this.nextEventNamed("turbo:load") - await this.waitUntilNoSelector(".turbo-progress-bar") - - this.assert.notOk(await this.hasSelector(".turbo-progress-bar"), "hides progress bar") - } - - async "test navigating does not render a progress bar before expiring the delay"() { - await this.remote.execute(() => window.Turbo.setProgressBarDelay(1000)) - await this.clickSelector("#same-origin-unannotated-link") - - this.assert.notOk(await this.hasSelector(".turbo-progress-bar"), "does not show progress bar before delay") - } - - async "test after loading the page"() { - this.assert.equal(await this.pathname, "/src/tests/fixtures/navigation.html") - this.assert.equal(await this.visitAction, "load") - } - - async "test following a same-origin unannotated link"() { - this.clickSelector("#same-origin-unannotated-link") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "advance") - this.assert.equal( - await this.nextAttributeMutationNamed("html", "aria-busy"), - "true", - "sets [aria-busy] on the document element" - ) - this.assert.equal( - await this.nextAttributeMutationNamed("html", "aria-busy"), - null, - "removes [aria-busy] from the document element" - ) - } - - async "test following a same-origin unannotated custom element link"() { - await this.nextBeat - await this.remote.execute(() => { - const shadowRoot = document.querySelector("#custom-link-element")?.shadowRoot - const link = shadowRoot?.querySelector("a") - link?.click() +import { test } from "@playwright/test" +import { assert } from "chai" +import { + clickWithoutScrolling, + hash, + hasSelector, + isScrolledToSelector, + nextAttributeMutationNamed, + nextBeat, + nextBody, + nextEventNamed, + noNextEventNamed, + pathname, + search, + selectorHasFocus, + visitAction, + waitUntilSelector, + waitUntilNoSelector, + willChangeBody, +} from "../helpers/page" + +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/navigation.html") +}) + +test("test navigating renders a progress bar", async ({ page }) => { + assert.equal( + await page.locator("style").evaluate((style) => style.nonce), + "123", + "renders progress bar stylesheet inline with nonce" + ) + + await page.evaluate(() => window.Turbo.setProgressBarDelay(0)) + await page.click("#delayed-link") + + await waitUntilSelector(page, ".turbo-progress-bar") + assert.ok(await hasSelector(page, ".turbo-progress-bar"), "displays progress bar") + + await nextEventNamed(page, "turbo:load") + await waitUntilNoSelector(page, ".turbo-progress-bar") + + assert.notOk(await hasSelector(page, ".turbo-progress-bar"), "hides progress bar") +}) + +test("test navigating does not render a progress bar before expiring the delay", async ({ page }) => { + await page.evaluate(() => window.Turbo.setProgressBarDelay(1000)) + await page.click("#same-origin-unannotated-link") + + assert.notOk(await hasSelector(page, ".turbo-progress-bar"), "does not show progress bar before delay") +}) + +test("test after loading the page", async ({ page }) => { + assert.equal(pathname(page.url()), "/src/tests/fixtures/navigation.html") + assert.equal(await visitAction(page), "load") +}) + +test("test following a same-origin unannotated link", async ({ page }) => { + page.click("#same-origin-unannotated-link") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "advance") + assert.equal( + await nextAttributeMutationNamed(page, "html", "aria-busy"), + "true", + "sets [aria-busy] on the document element" + ) + assert.equal( + await nextAttributeMutationNamed(page, "html", "aria-busy"), + null, + "removes [aria-busy] from the document element" + ) +}) + +test("test following a same-origin unannotated custom element link", async ({ page }) => { + await nextBeat() + await page.evaluate(() => { + const shadowRoot = document.querySelector("#custom-link-element")?.shadowRoot + const link = shadowRoot?.querySelector("a") + link?.click() + }) + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(search(page.url()), "") + assert.equal(await visitAction(page), "advance") +}) + +test("test following a same-origin unannotated link with search params", async ({ page }) => { + page.click("#same-origin-unannotated-link-search-params") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(search(page.url()), "?key=value") + assert.equal(await visitAction(page), "advance") +}) + +test("test following a same-origin unannotated form[method=GET]", async ({ page }) => { + page.click("#same-origin-unannotated-form button") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "advance") +}) + +test("test following a same-origin data-turbo-action=replace link", async ({ page }) => { + page.click("#same-origin-replace-link") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "replace") +}) + +test("test following a same-origin GET form[data-turbo-action=replace]", async ({ page }) => { + page.click("#same-origin-replace-form-get button") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "replace") +}) + +test("test following a same-origin GET form button[data-turbo-action=replace]", async ({ page }) => { + page.click("#same-origin-replace-form-submitter-get button") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "replace") +}) + +test("test following a same-origin POST form[data-turbo-action=replace]", async ({ page }) => { + page.click("#same-origin-replace-form-post button") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "replace") +}) + +test("test following a same-origin POST form button[data-turbo-action=replace]", async ({ page }) => { + page.click("#same-origin-replace-form-submitter-post button") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "replace") +}) + +test("test following a same-origin data-turbo=false link", async ({ page }) => { + page.click("#same-origin-false-link") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "load") +}) + +test("test following a same-origin unannotated link inside a data-turbo=false container", async ({ page }) => { + page.click("#same-origin-unannotated-link-inside-false-container") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "load") +}) + +test("test following a same-origin data-turbo=true link inside a data-turbo=false container", async ({ page }) => { + page.click("#same-origin-true-link-inside-false-container") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "advance") +}) + +test("test following a same-origin anchored link", async ({ page }) => { + await page.click("#same-origin-anchored-link") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(hash(page.url()), "#element-id") + assert.equal(await visitAction(page), "advance") + assert(await isScrolledToSelector(page, "#element-id")) +}) + +test("test following a same-origin link to a named anchor", async ({ page }) => { + await page.click("#same-origin-anchored-link-named") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(hash(page.url()), "#named-anchor") + assert.equal(await visitAction(page), "advance") + assert(await isScrolledToSelector(page, "[name=named-anchor]")) +}) + +test("test following a cross-origin unannotated link", async ({ page }) => { + await page.click("#cross-origin-unannotated-link") + await nextBody(page) + assert.equal(page.url(), "about:blank") + assert.equal(await visitAction(page), "load") +}) + +test("test following a same-origin [target] link", async ({ page }) => { + const [popup] = await Promise.all([page.waitForEvent("popup"), page.click("#same-origin-targeted-link")]) + + assert.equal(pathname(popup.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(popup), "load") +}) + +test("test following a same-origin [download] link", async ({ page }) => { + assert.notOk( + await willChangeBody(page, async () => { + await page.click("#same-origin-download-link") + await nextBeat() }) - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.search, "") - this.assert.equal(await this.visitAction, "advance") - } - - async "test following a same-origin unannotated link with search params"() { - this.clickSelector("#same-origin-unannotated-link-search-params") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.search, "?key=value") - this.assert.equal(await this.visitAction, "advance") - } - - async "test following a same-origin unannotated form[method=GET]"() { - this.clickSelector("#same-origin-unannotated-form button") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "advance") - } - - async "test following a same-origin data-turbo-action=replace link"() { - this.clickSelector("#same-origin-replace-link") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "replace") - } - - async "test following a same-origin GET form[data-turbo-action=replace]"() { - this.clickSelector("#same-origin-replace-form-get button") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "replace") - } - - async "test following a same-origin GET form button[data-turbo-action=replace]"() { - this.clickSelector("#same-origin-replace-form-submitter-get button") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "replace") - } - - async "test following a same-origin POST form[data-turbo-action=replace]"() { - this.clickSelector("#same-origin-replace-form-post button") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "replace") - } - - async "test following a same-origin POST form button[data-turbo-action=replace]"() { - this.clickSelector("#same-origin-replace-form-submitter-post button") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "replace") - } - - async "test following a same-origin data-turbo=false link"() { - this.clickSelector("#same-origin-false-link") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "load") - } - - async "test following a same-origin unannotated link inside a data-turbo=false container"() { - this.clickSelector("#same-origin-unannotated-link-inside-false-container") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "load") - } - - async "test following a same-origin data-turbo=true link inside a data-turbo=false container"() { - this.clickSelector("#same-origin-true-link-inside-false-container") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "advance") - } - - async "test following a same-origin anchored link"() { - this.clickSelector("#same-origin-anchored-link") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.hash, "#element-id") - this.assert.equal(await this.visitAction, "advance") - this.assert(await this.isScrolledToSelector("#element-id")) - } - - async "test following a same-origin link to a named anchor"() { - this.clickSelector("#same-origin-anchored-link-named") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.hash, "#named-anchor") - this.assert.equal(await this.visitAction, "advance") - this.assert(await this.isScrolledToSelector("[name=named-anchor]")) - } - - async "test following a cross-origin unannotated link"() { - this.clickSelector("#cross-origin-unannotated-link") - await this.nextBody - this.assert.equal(await this.location, "about:blank") - this.assert.equal(await this.visitAction, "load") - } - - async "test following a same-origin [target] link"() { - this.clickSelector("#same-origin-targeted-link") - await this.nextBeat - this.remote.switchToWindow(await this.nextWindowHandle) - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "load") - } - - async "test following a same-origin [download] link"() { - this.clickSelector("#same-origin-download-link") - await this.nextBeat - this.assert(!(await this.changedBody)) - this.assert.equal(await this.pathname, "/src/tests/fixtures/navigation.html") - this.assert.equal(await this.visitAction, "load") - } - - async "test following a same-origin link inside an SVG element"() { - this.clickSelector("#same-origin-link-inside-svg-element") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "advance") - } - - async "test following a cross-origin link inside an SVG element"() { - this.clickSelector("#cross-origin-link-inside-svg-element") - await this.nextBody - this.assert.equal(await this.location, "about:blank") - this.assert.equal(await this.visitAction, "load") - } - - async "test clicking the back button"() { - this.clickSelector("#same-origin-unannotated-link") - await this.nextBody - await this.goBack() - this.assert.equal(await this.pathname, "/src/tests/fixtures/navigation.html") - this.assert.equal(await this.visitAction, "restore") - } - - async "test clicking the forward button"() { - this.clickSelector("#same-origin-unannotated-link") - await this.nextBody - await this.goBack() - await this.goForward() - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "restore") - } - - async "test link targeting a disabled turbo-frame navigates the page"() { - await this.clickSelector("#link-to-disabled-frame") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/hello.html") - } - - async "test skip link with hash-only path scrolls to the anchor without a visit"() { - const bodyElementId = (await this.body).elementId - await this.clickSelector('a[href="#main"]') - await this.nextBeat - - this.assert.equal((await this.body).elementId, bodyElementId, "does not reload page") - this.assert.ok(await this.isScrolledToSelector("#main"), "scrolled to #main") - } - - async "test skip link with hash-only path moves focus and changes tab order"() { - await this.clickSelector('a[href="#main"]') - await this.nextBeat - await this.pressTab() - - this.assert.notOk(await this.selectorHasFocus("#ignored-link"), "skips interactive elements before #main") - this.assert.ok( - await this.selectorHasFocus("#main a:first-of-type"), - "skips to first interactive element after #main" - ) - } - - async "test same-page anchored replace link assumes the intention was a refresh"() { - await this.clickSelector("#refresh-link") - await this.nextBody - this.assert.ok(await this.isScrolledToSelector("#main"), "scrolled to #main") - } - - async "test navigating back to anchored URL"() { - await this.clickSelector('a[href="#main"]') - await this.nextBeat - - await this.clickSelector("#same-origin-unannotated-link") - await this.nextBody - await this.nextBeat - - await this.goBack() - await this.nextBody - - this.assert.ok(await this.isScrolledToSelector("#main"), "scrolled to #main") - } - - async "test following a redirection"() { - await this.clickSelector("#redirection-link") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "replace") - } - - async "test clicking the back button after redirection"() { - await this.clickSelector("#redirection-link") - await this.nextBody - await this.goBack() - this.assert.equal(await this.pathname, "/src/tests/fixtures/navigation.html") - this.assert.equal(await this.visitAction, "restore") - } - - async "test same-page anchor visits do not trigger visit events"() { - const events = [ - "turbo:before-visit", - "turbo:visit", - "turbo:before-cache", - "turbo:before-render", - "turbo:render", - "turbo:load", - ] - - for (const eventName in events) { - await this.goToLocation("/src/tests/fixtures/navigation.html") - await this.clickSelector('a[href="#main"]') - this.assert.ok(await this.noNextEventNamed(eventName), `same-page links do not trigger ${eventName} events`) - } - } - - async "test correct referrer header"() { - this.clickSelector("#headers-link") - await this.nextBody - const pre = await this.querySelector("pre") - const headers = await JSON.parse(await pre.getVisibleText()) - this.assert.equal( - headers.referer, - "http://localhost:9000/src/tests/fixtures/navigation.html", - `referer header is correctly set` - ) - } - - async "test double-clicking on a link"() { - this.clickSelector("#delayed-link") - this.clickSelector("#delayed-link") - - await this.nextBody - this.assert.equal(await this.pathname, "/__turbo/delayed_response") - this.assert.equal(await this.visitAction, "advance") - } - - async "test navigating back whilst a visit is in-flight"() { - this.clickSelector("#delayed-link") - await this.nextBeat - await this.goBack() - - this.assert.ok( - await this.nextEventNamed("turbo:visit"), - "navigating back whilst a visit is in-flight starts a non-silent Visit" - ) - - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/navigation.html") - this.assert.equal(await this.visitAction, "restore") - } -} - -NavigationTests.registerSuite() + ) + assert.equal(pathname(page.url()), "/src/tests/fixtures/navigation.html") + assert.equal(await visitAction(page), "load") +}) + +test("test following a same-origin link inside an SVG element", async ({ page }) => { + await page.click("#same-origin-link-inside-svg-element", { force: true }) + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "advance") +}) + +test("test following a cross-origin link inside an SVG element", async ({ page }) => { + await page.click("#cross-origin-link-inside-svg-element", { force: true }) + await nextBody(page) + assert.equal(page.url(), "about:blank") + assert.equal(await visitAction(page), "load") +}) + +test("test clicking the back button", async ({ page }) => { + await page.click("#same-origin-unannotated-link") + await nextBody(page) + await page.goBack() + assert.equal(pathname(page.url()), "/src/tests/fixtures/navigation.html") + assert.equal(await visitAction(page), "restore") +}) + +test("test clicking the forward button", async ({ page }) => { + await page.click("#same-origin-unannotated-link") + await nextBody(page) + await page.goBack() + await page.goForward() + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "restore") +}) + +test("test link targeting a disabled turbo-frame navigates the page", async ({ page }) => { + await page.click("#link-to-disabled-frame") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/hello.html") +}) + +test("test skip link with hash-only path scrolls to the anchor without a visit", async ({ page }) => { + assert.notOk( + await willChangeBody(page, async () => { + await page.click('a[href="#main"]') + await nextBeat() + }) + ) + + assert.ok(await isScrolledToSelector(page, "#main"), "scrolled to #main") +}) + +test("test skip link with hash-only path moves focus and changes tab order", async ({ page }) => { + await page.click('a[href="#main"]') + await nextBeat() + await page.press("#main", "Tab") + + assert.notOk(await selectorHasFocus(page, "#ignored-link"), "skips interactive elements before #main") + assert.ok( + await selectorHasFocus(page, "#same-origin-unannotated-link"), + "skips to first interactive element after #main" + ) +}) + +test("test same-page anchored replace link assumes the intention was a refresh", async ({ page }) => { + await page.click("#refresh-link") + await nextBody(page) + assert.ok(await isScrolledToSelector(page, "#main"), "scrolled to #main") +}) + +test("test navigating back to anchored URL", async ({ page }) => { + await clickWithoutScrolling(page, 'a[href="#main"]', { hasText: "Skip Link" }) + await nextBeat() + + await clickWithoutScrolling(page, "#same-origin-unannotated-link") + await nextBody(page) + await nextBeat() + + await page.goBack() + await nextBody(page) + + assert.ok(await isScrolledToSelector(page, "#main"), "scrolled to #main") +}) + +test("test following a redirection", async ({ page }) => { + await page.click("#redirection-link") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "replace") +}) + +test("test clicking the back button after redirection", async ({ page }) => { + await page.click("#redirection-link") + await nextBody(page) + await page.goBack() + assert.equal(pathname(page.url()), "/src/tests/fixtures/navigation.html") + assert.equal(await visitAction(page), "restore") +}) + +test("test same-page anchor visits do not trigger visit events", async ({ page }) => { + const events = [ + "turbo:before-visit", + "turbo:visit", + "turbo:before-cache", + "turbo:before-render", + "turbo:render", + "turbo:load", + ] + + for (const eventName in events) { + await page.goto("/src/tests/fixtures/navigation.html") + await page.click('a[href="#main"]') + assert.ok(await noNextEventNamed(page, eventName), `same-page links do not trigger ${eventName} events`) + } +}) + +test("test correct referrer header", async ({ page }) => { + page.click("#headers-link") + await nextBody(page) + const pre = await page.textContent("pre") + const headers = await JSON.parse(pre || "") + assert.equal( + headers.referer, + "http://localhost:9000/src/tests/fixtures/navigation.html", + `referer header is correctly set` + ) +}) + +test("test double-clicking on a link", async ({ page }) => { + page.click("#delayed-link") + page.click("#delayed-link") + + await nextBody(page, 1200) + assert.equal(pathname(page.url()), "/__turbo/delayed_response") + assert.equal(await visitAction(page), "advance") +}) + +test("test navigating back whilst a visit is in-flight", async ({ page }) => { + page.click("#delayed-link") + await nextEventNamed(page, "turbo:before-render") + await page.goBack() + + assert.ok( + await nextEventNamed(page, "turbo:visit"), + "navigating back whilst a visit is in-flight starts a non-silent Visit" + ) + + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/navigation.html") + assert.equal(await visitAction(page), "restore") +}) diff --git a/src/tests/functional/pausable_rendering_tests.ts b/src/tests/functional/pausable_rendering_tests.ts index bfa9594c6..2e3c307a0 100644 --- a/src/tests/functional/pausable_rendering_tests.ts +++ b/src/tests/functional/pausable_rendering_tests.ts @@ -1,37 +1,34 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" +import { nextBeat } from "../helpers/page" -export class PausableRenderingTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/pausable_rendering.html") - } +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/pausable_rendering.html") +}) - async "test pauses and resumes rendering"() { - await this.clickSelector("#link") +test("test pauses and resumes rendering", async ({ page }) => { + page.on("dialog", (dialog) => { + assert.strictEqual(dialog.message(), "Continue rendering?") + dialog.accept() + }) - await this.nextBeat - this.assert.strictEqual(await this.getAlertText(), "Continue rendering?") - await this.acceptAlert() + await page.click("#link") + await nextBeat() - await this.nextBeat - const h1 = await this.querySelector("h1") - this.assert.equal(await h1.getVisibleText(), "One") - } + assert.equal(await page.textContent("h1"), "One") +}) - async "test aborts rendering"() { - await this.clickSelector("#link") +test("test aborts rendering", async ({ page }) => { + const [firstDialog] = await Promise.all([page.waitForEvent("dialog"), page.click("#link")]) - await this.nextBeat - this.assert.strictEqual(await this.getAlertText(), "Continue rendering?") - await this.dismissAlert() + assert.strictEqual(firstDialog.message(), "Continue rendering?") - await this.nextBeat - this.assert.strictEqual(await this.getAlertText(), "Rendering aborted") - await this.acceptAlert() + firstDialog.dismiss() - await this.nextBeat - const h1 = await this.querySelector("h1") - this.assert.equal(await h1.getVisibleText(), "Pausable Rendering") - } -} + const nextDialog = await page.waitForEvent("dialog") -PausableRenderingTests.registerSuite() + assert.strictEqual(nextDialog.message(), "Rendering aborted") + nextDialog.accept() + + assert.equal(await page.textContent("h1"), "Pausable Rendering") +}) diff --git a/src/tests/functional/pausable_requests_tests.ts b/src/tests/functional/pausable_requests_tests.ts index 630cc981f..b7f330758 100644 --- a/src/tests/functional/pausable_requests_tests.ts +++ b/src/tests/functional/pausable_requests_tests.ts @@ -1,37 +1,38 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" +import { nextBeat } from "../helpers/page" -export class PausableRequestsTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/pausable_requests.html") - } +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/pausable_requests.html") +}) - async "test pauses and resumes request"() { - await this.clickSelector("#link") +test("test pauses and resumes request", async ({ page }) => { + page.once("dialog", (dialog) => { + assert.strictEqual(dialog.message(), "Continue request?") + dialog.accept() + }) - await this.nextBeat - this.assert.strictEqual(await this.getAlertText(), "Continue request?") - await this.acceptAlert() + await page.click("#link") + await nextBeat() - await this.nextBeat - const h1 = await this.querySelector("h1") - this.assert.equal(await h1.getVisibleText(), "One") - } + assert.equal(await page.textContent("h1"), "One") +}) - async "test aborts request"() { - await this.clickSelector("#link") +test("test aborts request", async ({ page }) => { + page.once("dialog", (dialog) => { + assert.strictEqual(dialog.message(), "Continue request?") + dialog.dismiss() + }) - await this.nextBeat - this.assert.strictEqual(await this.getAlertText(), "Continue request?") - await this.dismissAlert() + await page.click("#link") + await nextBeat() - await this.nextBeat - this.assert.strictEqual(await this.getAlertText(), "Request aborted") - await this.acceptAlert() + page.once("dialog", (dialog) => { + assert.strictEqual(dialog.message(), "Request aborted") + dialog.accept() + }) - await this.nextBeat - const h1 = await this.querySelector("h1") - this.assert.equal(await h1.getVisibleText(), "Pausable Requests") - } -} + await nextBeat() -PausableRequestsTests.registerSuite() + assert.equal(await page.textContent("h1"), "Pausable Requests") +}) diff --git a/src/tests/functional/preloader_tests.ts b/src/tests/functional/preloader_tests.ts index 36c961fb4..3faac3dfd 100644 --- a/src/tests/functional/preloader_tests.ts +++ b/src/tests/functional/preloader_tests.ts @@ -1,55 +1,53 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" - -export class PreloaderTests extends TurboDriveTestCase { - async "test preloads snapshot on initial load"() { - // contains `a[rel="preload"][href="http://localhost:9000/src/tests/fixtures/preloaded.html"]` - await this.goToLocation("/src/tests/fixtures/preloading.html") - await this.nextBeat - - this.assert.ok( - await this.remote.execute(() => { - const preloadedUrl = "http://localhost:9000/src/tests/fixtures/preloaded.html" - const cache = window.Turbo.session.preloader.snapshotCache.snapshots - - return preloadedUrl in cache - }) - ) - } - - async "test preloads snapshot on page visit"() { - // contains `a[rel="preload"][href="http://localhost:9000/src/tests/fixtures/preloading.html"]` - await this.goToLocation("/src/tests/fixtures/hot_preloading.html") - - // contains `a[rel="preload"][href="http://localhost:9000/src/tests/fixtures/preloaded.html"]` - await this.clickSelector("#hot_preload_anchor") - await this.waitUntilSelector("#preload_anchor") - await this.nextBeat - - this.assert.ok( - await this.remote.execute(() => { - const preloadedUrl = "http://localhost:9000/src/tests/fixtures/preloaded.html" - const cache = window.Turbo.session.preloader.snapshotCache.snapshots - - return preloadedUrl in cache - }) - ) - } - - async "test navigates to preloaded snapshot from frame"() { - // contains `a[rel="preload"][href="http://localhost:9000/src/tests/fixtures/preloaded.html"]` - await this.goToLocation("/src/tests/fixtures/frame_preloading.html") - await this.waitUntilSelector("#frame_preload_anchor") - await this.nextBeat - - this.assert.ok( - await this.remote.execute(() => { - const preloadedUrl = "http://localhost:9000/src/tests/fixtures/preloaded.html" - const cache = window.Turbo.session.preloader.snapshotCache.snapshots - - return preloadedUrl in cache - }) - ) - } -} - -PreloaderTests.registerSuite() +import { test } from "@playwright/test" +import { assert } from "chai" +import { nextBeat } from "../helpers/page" + +test("test preloads snapshot on initial load", async ({ page }) => { + // contains `a[rel="preload"][href="http://localhost:9000/src/tests/fixtures/preloaded.html"]` + await page.goto("/src/tests/fixtures/preloading.html") + await nextBeat() + + assert.ok( + await page.evaluate(() => { + const preloadedUrl = "http://localhost:9000/src/tests/fixtures/preloaded.html" + const cache = window.Turbo.session.preloader.snapshotCache.snapshots + + return preloadedUrl in cache + }) + ) +}) + +test("test preloads snapshot on page visit", async ({ page }) => { + // contains `a[rel="preload"][href="http://localhost:9000/src/tests/fixtures/preloading.html"]` + await page.goto("/src/tests/fixtures/hot_preloading.html") + + // contains `a[rel="preload"][href="http://localhost:9000/src/tests/fixtures/preloaded.html"]` + await page.click("#hot_preload_anchor") + await page.waitForSelector("#preload_anchor") + await nextBeat() + + assert.ok( + await page.evaluate(() => { + const preloadedUrl = "http://localhost:9000/src/tests/fixtures/preloaded.html" + const cache = window.Turbo.session.preloader.snapshotCache.snapshots + + return preloadedUrl in cache + }) + ) +}) + +test("test navigates to preloaded snapshot from frame", async ({ page }) => { + // contains `a[rel="preload"][href="http://localhost:9000/src/tests/fixtures/preloaded.html"]` + await page.goto("/src/tests/fixtures/frame_preloading.html") + await page.waitForSelector("#frame_preload_anchor") + await nextBeat() + + assert.ok( + await page.evaluate(() => { + const preloadedUrl = "http://localhost:9000/src/tests/fixtures/preloaded.html" + const cache = window.Turbo.session.preloader.snapshotCache.snapshots + + return preloadedUrl in cache + }) + ) +}) diff --git a/src/tests/functional/rendering_tests.ts b/src/tests/functional/rendering_tests.ts index 2a55ddb97..f881abbb2 100644 --- a/src/tests/functional/rendering_tests.ts +++ b/src/tests/functional/rendering_tests.ts @@ -1,387 +1,363 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" -import { Element } from "@theintern/leadfoot" - -export class RenderingTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/rendering.html") - } - - async teardown() { - await this.remote.execute(() => localStorage.clear()) - } - - async "test triggers before-render and render events"() { - this.clickSelector("#same-origin-link") - const { newBody } = await this.nextEventNamed("turbo:before-render") - - const h1 = await this.querySelector("h1") - this.assert.equal(await h1.getVisibleText(), "One") - - await this.nextEventNamed("turbo:render") - this.assert(await newBody.equals(await this.body)) - } - - async "test triggers before-render and render events for error pages"() { - this.clickSelector("#nonexistent-link") - const { newBody } = await this.nextEventNamed("turbo:before-render") - - this.assert.equal(await newBody.getVisibleText(), "404 Not Found: /nonexistent") - - await this.nextEventNamed("turbo:render") - this.assert(await newBody.equals(await this.body)) - } - - async "test reloads when tracked elements change"() { - await this.remote.execute(() => - window.addEventListener("turbo:reload", (e: any) => { +import { JSHandle, Page, test } from "@playwright/test" +import { assert } from "chai" +import { + clearLocalStorage, + disposeAll, + isScrolledToTop, + nextBeat, + nextBody, + nextEventNamed, + pathname, + scrollToSelector, + selectorHasFocus, + sleep, + strictElementEquals, + textContent, + visitAction, +} from "../helpers/page" + +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/rendering.html") + await clearLocalStorage(page) +}) + +test("test triggers before-render and render events", async ({ page }) => { + await page.click("#same-origin-link") + const { newBody } = await nextEventNamed(page, "turbo:before-render") + + assert.equal(await page.textContent("h1"), "One") + + await nextEventNamed(page, "turbo:render") + assert.equal(await newBody, await page.evaluate(() => document.body.outerHTML)) +}) + +test("test triggers before-render and render events for error pages", async ({ page }) => { + await page.click("#nonexistent-link") + const { newBody } = await nextEventNamed(page, "turbo:before-render") + + assert.equal(await textContent(page, newBody), "404 Not Found: /nonexistent\n") + + await nextEventNamed(page, "turbo:render") + assert.equal(await newBody, await page.evaluate(() => document.body.outerHTML)) +}) + +test("test reloads when tracked elements change", async ({ page }) => { + await page.evaluate(() => + window.addEventListener( + "turbo:reload", + (e: any) => { localStorage.setItem("reloadReason", e.detail.reason) - }) + }, + { once: true } ) + ) - this.clickSelector("#tracked-asset-change-link") - await this.nextBody + await page.click("#tracked-asset-change-link") + await nextBody(page) - const reason = await this.remote.execute(() => localStorage.getItem("reloadReason")) + const reason = await page.evaluate(() => localStorage.getItem("reloadReason")) - this.assert.equal(await this.pathname, "/src/tests/fixtures/tracked_asset_change.html") - this.assert.equal(await this.visitAction, "load") - this.assert.equal(reason, "tracked_element_mismatch") - } + assert.equal(pathname(page.url()), "/src/tests/fixtures/tracked_asset_change.html") + assert.equal(await visitAction(page), "load") + assert.equal(reason, "tracked_element_mismatch") +}) - async "test wont reload when tracked elements has a nonce"() { - this.clickSelector("#tracked-nonce-tag-link") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/tracked_nonce_tag.html") - this.assert.equal(await this.visitAction, "advance") - } +test("test wont reload when tracked elements has a nonce", async ({ page }) => { + await page.click("#tracked-nonce-tag-link") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/tracked_nonce_tag.html") + assert.equal(await visitAction(page), "advance") +}) - async "test reloads when turbo-visit-control setting is reload"() { - await this.remote.execute(() => - window.addEventListener("turbo:reload", (e: any) => { +test("test reloads when turbo-visit-control setting is reload", async ({ page }) => { + await page.evaluate(() => + window.addEventListener( + "turbo:reload", + (e: any) => { localStorage.setItem("reloadReason", e.detail.reason) - }) + }, + { once: true } ) + ) - this.clickSelector("#visit-control-reload-link") - await this.nextBody + await page.click("#visit-control-reload-link") + await nextBody(page) - const reason = await this.remote.execute(() => localStorage.getItem("reloadReason")) + const reason = await page.evaluate(() => localStorage.getItem("reloadReason")) - this.assert.equal(await this.pathname, "/src/tests/fixtures/visit_control_reload.html") - this.assert.equal(await this.visitAction, "load") - this.assert.equal(reason, "turbo_visit_control_is_reload") - } + assert.equal(pathname(page.url()), "/src/tests/fixtures/visit_control_reload.html") + assert.equal(await visitAction(page), "load") + assert.equal(reason, "turbo_visit_control_is_reload") +}) - async "test maintains scroll position before visit when turbo-visit-control setting is reload"() { - await this.scrollToSelector("#below-the-fold-visit-control-reload-link") - this.assert.notOk(await this.isScrolledToTop(), "scrolled down") +test("test maintains scroll position before visit when turbo-visit-control setting is reload", async ({ page }) => { + await scrollToSelector(page, "#below-the-fold-visit-control-reload-link") + assert.notOk(await isScrolledToTop(page), "scrolled down") - await this.remote.execute(() => localStorage.setItem("scrolls", "false")) + await page.evaluate(() => localStorage.setItem("scrolls", "false")) - this.remote.execute(() => + page.evaluate(() => + addEventListener("click", () => { addEventListener("scroll", () => { localStorage.setItem("scrolls", "true") }) - ) - - this.clickSelector("#below-the-fold-visit-control-reload-link") - - await this.nextBody - - const scrolls = await this.remote.execute(() => localStorage.getItem("scrolls")) - this.assert.ok(scrolls === "false", "scroll position is preserved") - - this.assert.equal(await this.pathname, "/src/tests/fixtures/visit_control_reload.html") - this.assert.equal(await this.visitAction, "load") - } - - async "test accumulates asset elements in head"() { - const originalElements = await this.assetElements - - this.clickSelector("#additional-assets-link") - await this.nextBody - const newElements = await this.assetElements - this.assert.notDeepEqual(newElements, originalElements) - - this.goBack() - await this.nextBody - const finalElements = await this.assetElements - this.assert.deepEqual(finalElements, newElements) - } - - async "test replaces provisional elements in head"() { - const originalElements = await this.provisionalElements - this.assert(!(await this.hasSelector("meta[name=test]"))) - - this.clickSelector("#same-origin-link") - await this.nextBody - const newElements = await this.provisionalElements - this.assert.notDeepEqual(newElements, originalElements) - this.assert(await this.hasSelector("meta[name=test]")) - - this.goBack() - await this.nextBody - const finalElements = await this.provisionalElements - this.assert.notDeepEqual(finalElements, newElements) - this.assert(!(await this.hasSelector("meta[name=test]"))) - } - - async "test evaluates head stylesheet elements"() { - this.assert.equal(await this.isStylesheetEvaluated, false) - - this.clickSelector("#additional-assets-link") - await this.nextEventNamed("turbo:render") - this.assert.equal(await this.isStylesheetEvaluated, true) - } - - async "test does not evaluate head stylesheet elements inside noscript elements"() { - this.assert.equal(await this.isNoscriptStylesheetEvaluated, false) - - this.clickSelector("#additional-assets-link") - await this.nextEventNamed("turbo:render") - this.assert.equal(await this.isNoscriptStylesheetEvaluated, false) - } - - async "skip evaluates head script elements once"() { - this.assert.equal(await this.headScriptEvaluationCount, undefined) - - this.clickSelector("#head-script-link") - await this.nextEventNamed("turbo:render") - this.assert.equal(await this.headScriptEvaluationCount, 1) - - this.goBack() - await this.nextEventNamed("turbo:render") - this.assert.equal(await this.headScriptEvaluationCount, 1) - - this.clickSelector("#head-script-link") - await this.nextEventNamed("turbo:render") - this.assert.equal(await this.headScriptEvaluationCount, 1) - } - - async "test evaluates body script elements on each render"() { - this.assert.equal(await this.bodyScriptEvaluationCount, undefined) - - this.clickSelector("#body-script-link") - await this.nextEventNamed("turbo:render") - this.assert.equal(await this.bodyScriptEvaluationCount, 1) - - this.goBack() - await this.nextEventNamed("turbo:render") - this.assert.equal(await this.bodyScriptEvaluationCount, 1) - - this.clickSelector("#body-script-link") - await this.nextEventNamed("turbo:render") - this.assert.equal(await this.bodyScriptEvaluationCount, 2) - } - - async "test does not evaluate data-turbo-eval=false scripts"() { - this.clickSelector("#eval-false-script-link") - await this.nextEventNamed("turbo:render") - this.assert.equal(await this.bodyScriptEvaluationCount, undefined) - } - - async "test preserves permanent elements"() { - const permanentElement = await this.permanentElement - this.assert.equal(await permanentElement.getVisibleText(), "Rendering") - - this.clickSelector("#permanent-element-link") - await this.nextEventNamed("turbo:render") - this.assert(await permanentElement.equals(await this.permanentElement)) - this.assert.equal(await permanentElement.getVisibleText(), "Rendering") - - this.goBack() - await this.nextEventNamed("turbo:render") - this.assert(await permanentElement.equals(await this.permanentElement)) - } + }) + ) - async "test restores focus during page rendering when transposing the activeElement"() { - await this.clickSelector("#permanent-input") - await this.pressEnter() - await this.nextBody + page.click("#below-the-fold-visit-control-reload-link") - this.assert.ok(await this.selectorHasFocus("#permanent-input"), "restores focus after page loads") - } + await nextBody(page) - async "test restores focus during page rendering when transposing an ancestor of the activeElement"() { - await this.clickSelector("#permanent-descendant-input") - await this.pressEnter() - await this.nextBody + const scrolls = await page.evaluate(() => localStorage.getItem("scrolls")) + assert.equal(scrolls, "false", "scroll position is preserved") - this.assert.ok(await this.selectorHasFocus("#permanent-descendant-input"), "restores focus after page loads") - } + assert.equal(pathname(page.url()), "/src/tests/fixtures/visit_control_reload.html") + assert.equal(await visitAction(page), "load") +}) - async "test preserves permanent elements within turbo-frames"() { - let permanentElement = await this.querySelector("#permanent-in-frame") - this.assert.equal(await permanentElement.getVisibleText(), "Rendering") +test("test accumulates asset elements in head", async ({ page }) => { + const assetElements = () => page.$$('script, style, link[rel="stylesheet"]') + const originalElements = await assetElements() + + await page.click("#additional-assets-link") + await nextBody(page) + const newElements = await assetElements() + assert.notOk(await deepElementsEqual(page, newElements, originalElements)) - await this.clickSelector("#permanent-in-frame-element-link") - await this.nextBeat - permanentElement = await this.querySelector("#permanent-in-frame") - this.assert.equal(await permanentElement.getVisibleText(), "Rendering") - } + await page.goBack() + await nextBody(page) + const finalElements = await assetElements() + assert.ok(await deepElementsEqual(page, finalElements, newElements)) - async "test preserves permanent elements within turbo-frames rendered without layouts"() { - let permanentElement = await this.querySelector("#permanent-in-frame") - this.assert.equal(await permanentElement.getVisibleText(), "Rendering") + await disposeAll(...originalElements, ...newElements, ...finalElements) +}) + +test("test replaces provisional elements in head", async ({ page }) => { + const provisionalElements = () => page.$$('head :not(script), head :not(style), head :not(link[rel="stylesheet"])') + const originalElements = await provisionalElements() + assert.equal(await page.locator("meta[name=test]").count(), 0) - await this.clickSelector("#permanent-in-frame-without-layout-element-link") - await this.nextBeat - permanentElement = await this.querySelector("#permanent-in-frame") - this.assert.equal(await permanentElement.getVisibleText(), "Rendering") - } + await page.click("#same-origin-link") + await nextBody(page) + const newElements = await provisionalElements() + assert.notOk(await deepElementsEqual(page, newElements, originalElements)) + assert.equal(await page.locator("meta[name=test]").count(), 1) - async "test restores focus during turbo-frame rendering when transposing the activeElement"() { - await this.clickSelector("#permanent-input-in-frame") - await this.pressEnter() - await this.nextBeat + await page.goBack() + await nextBody(page) + const finalElements = await provisionalElements() + assert.notOk(await deepElementsEqual(page, finalElements, newElements)) + assert.equal(await page.locator("meta[name=test]").count(), 0) - this.assert.ok(await this.selectorHasFocus("#permanent-input-in-frame"), "restores focus after page loads") - } + await disposeAll(...originalElements, ...newElements, ...finalElements) +}) - async "test restores focus during turbo-frame rendering when transposing a descendant of the activeElement"() { - await this.clickSelector("#permanent-descendant-input-in-frame") - await this.pressEnter() - await this.nextBeat +test("test evaluates head stylesheet elements", async ({ page }) => { + assert.equal(await isStylesheetEvaluated(page), false) - this.assert.ok( - await this.selectorHasFocus("#permanent-descendant-input-in-frame"), - "restores focus after page loads" - ) - } + await page.click("#additional-assets-link") + await nextEventNamed(page, "turbo:render") + assert.equal(await isStylesheetEvaluated(page), true) +}) - async "test preserves permanent element video playback"() { - let videoElement = await this.querySelector("#permanent-video") - await this.clickSelector("#permanent-video-button") - await this.sleep(500) +test("test does not evaluate head stylesheet elements inside noscript elements", async ({ page }) => { + assert.equal(await isNoscriptStylesheetEvaluated(page), false) - const timeBeforeRender = await videoElement.getProperty("currentTime") - this.assert.notEqual(timeBeforeRender, 0, "playback has started") + await page.click("#additional-assets-link") + await nextEventNamed(page, "turbo:render") + assert.equal(await isNoscriptStylesheetEvaluated(page), false) +}) - await this.clickSelector("#permanent-element-link") - await this.nextBody - videoElement = await this.querySelector("#permanent-video") +test("skip evaluates head script elements once", async ({ page }) => { + assert.equal(await headScriptEvaluationCount(page), undefined) - const timeAfterRender = await videoElement.getProperty("currentTime") - this.assert.equal(timeAfterRender, timeBeforeRender, "element state is preserved") - } + await page.click("#head-script-link") + await nextEventNamed(page, "turbo:render") + assert.equal(await headScriptEvaluationCount(page), 1) - async "test before-cache event"() { - this.beforeCache((body) => (body.innerHTML = "Modified")) - this.clickSelector("#same-origin-link") - await this.nextBody - await this.goBack() - const body = await this.nextBody - this.assert(await body.getVisibleText(), "Modified") - } + await page.goBack() + await nextEventNamed(page, "turbo:render") + assert.equal(await headScriptEvaluationCount(page), 1) - async "test mutation record as before-cache notification"() { - this.modifyBodyAfterRemoval() - this.clickSelector("#same-origin-link") - await this.nextBody - await this.goBack() - const body = await this.nextBody - this.assert(await body.getVisibleText(), "Modified") - } + await page.click("#head-script-link") + await nextEventNamed(page, "turbo:render") + assert.equal(await headScriptEvaluationCount(page), 1) +}) - async "test error pages"() { - this.clickSelector("#nonexistent-link") - const body = await this.nextBody - this.assert.equal(await body.getVisibleText(), "404 Not Found: /nonexistent") - await this.goBack() - } +test("test evaluates body script elements on each render", async ({ page }) => { + assert.equal(await bodyScriptEvaluationCount(page), undefined) - get assetElements(): Promise { - return filter(this.headElements, isAssetElement) - } + await page.click("#body-script-link") + await nextEventNamed(page, "turbo:render") + assert.equal(await bodyScriptEvaluationCount(page), 1) - get provisionalElements(): Promise { - return filter(this.headElements, async (element) => !(await isAssetElement(element))) - } + await page.goBack() + await nextEventNamed(page, "turbo:render") + assert.equal(await bodyScriptEvaluationCount(page), 1) - get headElements(): Promise { - return this.evaluate(() => Array.from(document.head.children) as any[]) - } + await page.click("#body-script-link") + await nextEventNamed(page, "turbo:render") + assert.equal(await bodyScriptEvaluationCount(page), 2) +}) - get permanentElement(): Promise { - return this.querySelector("#permanent") - } +test("test does not evaluate data-turbo-eval=false scripts", async ({ page }) => { + await page.click("#eval-false-script-link") + await nextEventNamed(page, "turbo:render") + assert.equal(await bodyScriptEvaluationCount(page), undefined) +}) - get headScriptEvaluationCount(): Promise { - return this.evaluate(() => window.headScriptEvaluationCount) - } - - get bodyScriptEvaluationCount(): Promise { - return this.evaluate(() => window.bodyScriptEvaluationCount) - } - - get isStylesheetEvaluated(): Promise { - return this.evaluate( - () => getComputedStyle(document.body).getPropertyValue("--black-if-evaluated").trim() === "black" - ) - } - - get isNoscriptStylesheetEvaluated(): Promise { - return this.evaluate( - () => getComputedStyle(document.body).getPropertyValue("--black-if-noscript-evaluated").trim() === "black" - ) - } +test("test preserves permanent elements", async ({ page }) => { + const permanentElement = await page.locator("#permanent") + assert.equal(await permanentElement.textContent(), "Rendering") + + await page.click("#permanent-element-link") + await nextEventNamed(page, "turbo:render") + assert.ok(await strictElementEquals(permanentElement, await page.locator("#permanent"))) + assert.equal(await permanentElement!.textContent(), "Rendering") + + await page.goBack() + await nextEventNamed(page, "turbo:render") + assert.ok(await strictElementEquals(permanentElement, await page.locator("#permanent"))) +}) + +test("test restores focus during page rendering when transposing the activeElement", async ({ page }) => { + await page.press("#permanent-input", "Enter") + await nextBody(page) + + assert.ok(await selectorHasFocus(page, "#permanent-input"), "restores focus after page loads") +}) + +test("test restores focus during page rendering when transposing an ancestor of the activeElement", async ({ + page, +}) => { + await page.press("#permanent-descendant-input", "Enter") + await nextBody(page) + + assert.ok(await selectorHasFocus(page, "#permanent-descendant-input"), "restores focus after page loads") +}) + +test("test preserves permanent elements within turbo-frames", async ({ page }) => { + assert.equal(await page.textContent("#permanent-in-frame"), "Rendering") + + await page.click("#permanent-in-frame-element-link") + await nextBeat() + + assert.equal(await page.textContent("#permanent-in-frame"), "Rendering") +}) + +test("test preserves permanent elements within turbo-frames rendered without layouts", async ({ page }) => { + assert.equal(await page.textContent("#permanent-in-frame"), "Rendering") + + await page.click("#permanent-in-frame-without-layout-element-link") + await nextBeat() + + assert.equal(await page.textContent("#permanent-in-frame"), "Rendering") +}) + +test("test restores focus during turbo-frame rendering when transposing the activeElement", async ({ page }) => { + await page.press("#permanent-input-in-frame", "Enter") + await nextBeat() + + assert.ok(await selectorHasFocus(page, "#permanent-input-in-frame"), "restores focus after page loads") +}) + +test("test restores focus during turbo-frame rendering when transposing a descendant of the activeElement", async ({ + page, +}) => { + await page.press("#permanent-descendant-input-in-frame", "Enter") + await nextBeat() + + assert.ok(await selectorHasFocus(page, "#permanent-descendant-input-in-frame"), "restores focus after page loads") +}) + +test("test preserves permanent element video playback", async ({ page }) => { + const videoElement = await page.locator("#permanent-video") + await page.click("#permanent-video-button") + await sleep(500) + + const timeBeforeRender = await videoElement.evaluate((video: HTMLVideoElement) => video.currentTime) + assert.notEqual(timeBeforeRender, 0, "playback has started") + + await page.click("#permanent-element-link") + await nextBody(page) + + const timeAfterRender = await videoElement.evaluate((video: HTMLVideoElement) => video.currentTime) + assert.equal(timeAfterRender, timeBeforeRender, "element state is preserved") +}) + +test("test before-cache event", async ({ page }) => { + await page.evaluate(() => { + addEventListener("turbo:before-cache", () => (document.body.innerHTML = "Modified"), { once: true }) + }) + await page.click("#same-origin-link") + await nextBody(page) + await page.goBack() + + assert.equal(await page.textContent("body"), "Modified") +}) + +test("test mutation record as before-cache notification", async ({ page }) => { + await modifyBodyAfterRemoval(page) + await page.click("#same-origin-link") + await nextBody(page) + await page.goBack() + + assert.equal(await page.textContent("body"), "Modified") +}) + +test("test error pages", async ({ page }) => { + await page.click("#nonexistent-link") + await nextBody(page) + + assert.equal(await page.textContent("body"), "404 Not Found: /nonexistent\n") +}) + +function deepElementsEqual( + page: Page, + left: JSHandle[], + right: JSHandle[] +): Promise { + return page.evaluate( + ([left, right]) => left.length == right.length && left.every((element) => right.includes(element)), + [left, right] + ) +} - async modifyBodyBeforeCaching() { - return this.remote.execute(() => - addEventListener( - "turbo:before-cache", - function eventListener() { - removeEventListener("turbo:before-cache", eventListener, false) - document.body.innerHTML = "Modified" - }, - false - ) - ) - } +function headScriptEvaluationCount(page: Page): Promise { + return page.evaluate(() => window.headScriptEvaluationCount) +} - async beforeCache(callback: (body: HTMLElement) => void) { - return this.remote.execute( - (callback: (body: HTMLElement) => void) => { - addEventListener( - "turbo:before-cache", - function eventListener() { - removeEventListener("turbo:before-cache", eventListener, false) - callback(document.body) - }, - false - ) - }, - [callback] - ) - } +function bodyScriptEvaluationCount(page: Page): Promise { + return page.evaluate(() => window.bodyScriptEvaluationCount) +} - async modifyBodyAfterRemoval() { - return this.remote.execute(() => { - const { documentElement, body } = document - const observer = new MutationObserver((records) => { - for (const record of records) { - if (Array.from(record.removedNodes).indexOf(body) > -1) { - body.innerHTML = "Modified" - observer.disconnect() - break - } - } - }) - observer.observe(documentElement, { childList: true }) - }) - } +function isStylesheetEvaluated(page: Page): Promise { + return page.evaluate( + () => getComputedStyle(document.body).getPropertyValue("--black-if-evaluated").trim() === "black" + ) } -async function filter(promisedValues: Promise, predicate: (value: T) => Promise): Promise { - const values = await promisedValues - const matches = await Promise.all(values.map((value) => predicate(value))) - return matches.reduce((result, match, index) => result.concat(match ? values[index] : []), [] as T[]) +function isNoscriptStylesheetEvaluated(page: Page): Promise { + return page.evaluate( + () => getComputedStyle(document.body).getPropertyValue("--black-if-noscript-evaluated").trim() === "black" + ) } -async function isAssetElement(element: Element): Promise { - const tagName = await element.getTagName() - const relValue = await element.getAttribute("rel") - return tagName == "script" || tagName == "style" || (tagName == "link" && relValue == "stylesheet") +function modifyBodyAfterRemoval(page: Page) { + return page.evaluate(() => { + const { documentElement, body } = document + const observer = new MutationObserver((records) => { + for (const record of records) { + if (Array.from(record.removedNodes).indexOf(body) > -1) { + body.innerHTML = "Modified" + observer.disconnect() + break + } + } + }) + observer.observe(documentElement, { childList: true }) + }) } declare global { @@ -390,5 +366,3 @@ declare global { bodyScriptEvaluationCount?: number } } - -RenderingTests.registerSuite() diff --git a/src/tests/functional/scroll_restoration_tests.ts b/src/tests/functional/scroll_restoration_tests.ts index 9f10fe5f2..d8b82bb4c 100644 --- a/src/tests/functional/scroll_restoration_tests.ts +++ b/src/tests/functional/scroll_restoration_tests.ts @@ -1,33 +1,31 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" +import { nextBeat, scrollPosition, scrollToSelector } from "../helpers/page" -export class ScrollRestorationTests extends TurboDriveTestCase { - async "test landing on an anchor"() { - await this.goToLocation("/src/tests/fixtures/scroll_restoration.html#three") - await this.nextBody - const { y: yAfterLoading } = await this.scrollPosition - this.assert.notEqual(yAfterLoading, 0) - } +test("test landing on an anchor", async ({ page }) => { + await page.goto("/src/tests/fixtures/scroll_restoration.html#three") + await nextBeat() + const { y: yAfterLoading } = await scrollPosition(page) + assert.notEqual(yAfterLoading, 0) +}) - async "test reloading after scrolling"() { - await this.goToLocation("/src/tests/fixtures/scroll_restoration.html") - await this.scrollToSelector("#three") - const { y: yAfterScrolling } = await this.scrollPosition - this.assert.notEqual(yAfterScrolling, 0) +test("test reloading after scrolling", async ({ page }) => { + await page.goto("/src/tests/fixtures/scroll_restoration.html") + await scrollToSelector(page, "#three") + const { y: yAfterScrolling } = await scrollPosition(page) + assert.notEqual(yAfterScrolling, 0) - await this.reload() - const { y: yAfterReloading } = await this.scrollPosition - this.assert.notEqual(yAfterReloading, 0) - } + await page.reload() + const { y: yAfterReloading } = await scrollPosition(page) + assert.notEqual(yAfterReloading, 0) +}) - async "test returning from history"() { - await this.goToLocation("/src/tests/fixtures/scroll_restoration.html") - await this.scrollToSelector("#three") - await this.goToLocation("/src/tests/fixtures/bare.html") - await this.goBack() +test("test returning from history", async ({ page }) => { + await page.goto("/src/tests/fixtures/scroll_restoration.html") + await scrollToSelector(page, "#three") + await page.goto("/src/tests/fixtures/bare.html") + await page.goBack() - const { y: yAfterReturning } = await this.scrollPosition - this.assert.notEqual(yAfterReturning, 0) - } -} - -ScrollRestorationTests.registerSuite() + const { y: yAfterReturning } = await scrollPosition(page) + assert.notEqual(yAfterReturning, 0) +}) diff --git a/src/tests/functional/stream_tests.ts b/src/tests/functional/stream_tests.ts index 74acc3350..be1aa8e2d 100644 --- a/src/tests/functional/stream_tests.ts +++ b/src/tests/functional/stream_tests.ts @@ -1,68 +1,63 @@ -import { FunctionalTestCase } from "../helpers/functional_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" +import { nextBeat } from "../helpers/page" -export class StreamTests extends FunctionalTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/stream.html") - } +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/stream.html") +}) - async "test receiving a stream message"() { - let element - const selector = "#messages div.message:last-child" +test("test receiving a stream message", async ({ page }) => { + const selector = "#messages div.message:last-child" - element = await this.querySelector(selector) - this.assert.equal(await element.getVisibleText(), "First") + assert.equal(await page.textContent(selector), "First") - await this.clickSelector("#create [type=submit]") - await this.nextBeat + await page.click("#create [type=submit]") + await nextBeat() - element = await this.querySelector(selector) - this.assert.equal(await element.getVisibleText(), "Hello world!") - } + assert.equal(await page.textContent(selector), "Hello world!") +}) - async "test receiving a stream message with css selector target"() { - let element - const selector = ".messages div.message:last-child" +test("test receiving a stream message with css selector target", async ({ page }) => { + let element + const selector = ".messages div.message:last-child" - element = await this.querySelectorAll(selector) - this.assert.equal(await element[0].getVisibleText(), "Second") - this.assert.equal(await element[1].getVisibleText(), "Third") + element = await page.locator(selector).allTextContents() + assert.equal(await element[0], "Second") + assert.equal(await element[1], "Third") - await this.clickSelector("#replace [type=submit]") - await this.nextBeat + await page.click("#replace [type=submit]") + await nextBeat() - element = await this.querySelectorAll(selector) - this.assert.equal(await element[0].getVisibleText(), "Hello CSS!") - this.assert.equal(await element[1].getVisibleText(), "Hello CSS!") - } + element = await page.locator(selector).allTextContents() + assert.equal(await element[0], "Hello CSS!") + assert.equal(await element[1], "Hello CSS!") +}) - async "test receiving a stream message asynchronously"() { - let messages = await this.querySelectorAll("#messages > *") +test("test receiving a stream message asynchronously", async ({ page }) => { + let messages = await page.locator("#messages > *").allTextContents() - this.assert.ok(messages[0]) - this.assert.notOk(messages[1], "receives streams when connected") - this.assert.notOk(messages[2], "receives streams when connected") + assert.ok(messages[0]) + assert.notOk(messages[1], "receives streams when connected") + assert.notOk(messages[2], "receives streams when connected") - await this.clickSelector("#async button") - await this.nextBeat + await page.click("#async button") + await nextBeat() - messages = await this.querySelectorAll("#messages > *") + messages = await page.locator("#messages > *").allTextContents() - this.assert.ok(messages[0]) - this.assert.ok(messages[1], "receives streams when connected") - this.assert.notOk(messages[2], "receives streams when connected") + assert.ok(messages[0]) + assert.ok(messages[1], "receives streams when connected") + assert.notOk(messages[2], "receives streams when connected") - await this.evaluate(() => document.getElementById("stream-source")?.remove()) - await this.nextBeat + await page.evaluate(() => document.getElementById("stream-source")?.remove()) + await nextBeat() - await this.clickSelector("#async button") - await this.nextBeat + await page.click("#async button") + await nextBeat() - messages = await this.querySelectorAll("#messages > *") + messages = await page.locator("#messages > *").allTextContents() - this.assert.ok(messages[0]) - this.assert.ok(messages[1], "receives streams when connected") - this.assert.notOk(messages[2], "does not receive streams when disconnected") - } -} - -StreamTests.registerSuite() + assert.ok(messages[0]) + assert.ok(messages[1], "receives streams when connected") + assert.notOk(messages[2], "does not receive streams when disconnected") +}) diff --git a/src/tests/functional/visit_tests.ts b/src/tests/functional/visit_tests.ts index 0b4da6f0f..b5603121d 100644 --- a/src/tests/functional/visit_tests.ts +++ b/src/tests/functional/visit_tests.ts @@ -1,153 +1,142 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { Page, test } from "@playwright/test" +import { assert } from "chai" import { get } from "http" +import { nextBeat, nextEventNamed, readEventLogs, visitAction, willChangeBody } from "../helpers/page" -export class VisitTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/visit.html") - } - - async "test programmatically visiting a same-origin location"() { - const urlBeforeVisit = await this.location - await this.visitLocation("/src/tests/fixtures/one.html") - - await this.nextBeat - - const urlAfterVisit = await this.location - this.assert.notEqual(urlBeforeVisit, urlAfterVisit) - this.assert.equal(await this.visitAction, "advance") - - const { url: urlFromBeforeVisitEvent } = await this.nextEventNamed("turbo:before-visit") - this.assert.equal(urlFromBeforeVisitEvent, urlAfterVisit) - - const { url: urlFromVisitEvent } = await this.nextEventNamed("turbo:visit") - this.assert.equal(urlFromVisitEvent, urlAfterVisit) - - const { timing } = await this.nextEventNamed("turbo:load") - this.assert.ok(timing) - } - - async "skip programmatically visiting a cross-origin location falls back to window.location"() { - const urlBeforeVisit = await this.location - await this.visitLocation("about:blank") - - const urlAfterVisit = await this.location - this.assert.notEqual(urlBeforeVisit, urlAfterVisit) - this.assert.equal(await this.visitAction, "load") - } - - async "test visiting a location served with a non-HTML content type"() { - const urlBeforeVisit = await this.location - await this.visitLocation("/src/tests/fixtures/svg.svg") - await this.nextBeat - - const url = await this.remote.getCurrentUrl() - const contentType = await contentTypeOfURL(url) - this.assert.equal(contentType, "image/svg+xml") - - const urlAfterVisit = await this.location - this.assert.notEqual(urlBeforeVisit, urlAfterVisit) - this.assert.equal(await this.visitAction, "load") - } - - async "test canceling a before-visit event prevents navigation"() { - this.cancelNextVisit() - const urlBeforeVisit = await this.location - - this.clickSelector("#same-origin-link") - await this.nextBeat - this.assert(!(await this.changedBody)) - - const urlAfterVisit = await this.location - this.assert.equal(urlAfterVisit, urlBeforeVisit) - } - - async "test navigation by history is not cancelable"() { - this.clickSelector("#same-origin-link") - await this.drainEventLog() - await this.nextBeat - - this.cancelNextVisit() - await this.goBack() - this.assert(await this.changedBody) - } - - async "test turbo:before-fetch-request event.detail"() { - await this.clickSelector("#same-origin-link") - const { url, fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") - - this.assert.equal(fetchOptions.method, "GET") - this.assert.ok(url.toString().includes("/src/tests/fixtures/one.html")) - } - - async "test turbo:before-fetch-request event.detail encodes searchParams"() { - await this.clickSelector("#same-origin-link-search-params") - const { url } = await this.nextEventNamed("turbo:before-fetch-request") - - this.assert.ok(url.includes("/src/tests/fixtures/one.html?key=value")) - } - - async "test turbo:before-fetch-response open new site"() { - this.remote.execute(() => - addEventListener( - "turbo:before-fetch-response", - async function eventListener(event: any) { - removeEventListener("turbo:before-fetch-response", eventListener, false) - ;(window as any).fetchResponseResult = { - responseText: await event.detail.fetchResponse.responseText, - responseHTML: await event.detail.fetchResponse.responseHTML, - } - }, - false - ) - ) +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/visit.html") + await readEventLogs(page) +}) - await this.clickSelector("#sample-response") - await this.nextEventNamed("turbo:before-fetch-response") +test("test programmatically visiting a same-origin location", async ({ page }) => { + const urlBeforeVisit = page.url() + await visitLocation(page, "/src/tests/fixtures/one.html") - const fetchResponseResult = await this.evaluate(() => (window as any).fetchResponseResult) + await nextBeat() - this.assert.isTrue(fetchResponseResult.responseText.indexOf("An element with an ID") > -1) - this.assert.isTrue(fetchResponseResult.responseHTML.indexOf("An element with an ID") > -1) - } + const urlAfterVisit = page.url() + assert.notEqual(urlBeforeVisit, urlAfterVisit) + assert.equal(await visitAction(page), "advance") - async "test cache does not override response after redirect"() { - await this.remote.execute(() => { - const cachedElement = document.createElement("some-cached-element") - document.body.appendChild(cachedElement) - }) + const { url: urlFromBeforeVisitEvent } = await nextEventNamed(page, "turbo:before-visit") + assert.equal(urlFromBeforeVisitEvent, urlAfterVisit) - this.assert(await this.hasSelector("some-cached-element")) - this.clickSelector("#same-origin-link") - await this.nextBeat - this.clickSelector("#redirection-link") - await this.nextBeat // 301 redirect response - await this.nextBeat // 200 response - this.assert.notOk(await this.hasSelector("some-cached-element")) - } - - async visitLocation(location: string) { - this.remote.execute((location: string) => window.Turbo.visit(location), [location]) - } - - async cancelNextVisit() { - this.remote.execute(() => - addEventListener( - "turbo:before-visit", - function eventListener(event) { - removeEventListener("turbo:before-visit", eventListener, false) - event.preventDefault() - }, - false - ) - ) - } + const { url: urlFromVisitEvent } = await nextEventNamed(page, "turbo:visit") + assert.equal(urlFromVisitEvent, urlAfterVisit) + + const { timing } = await nextEventNamed(page, "turbo:load") + assert.ok(timing) +}) + +test("skip programmatically visiting a cross-origin location falls back to window.location", async ({ page }) => { + const urlBeforeVisit = page.url() + await visitLocation(page, "about:blank") + + const urlAfterVisit = page.url() + assert.notEqual(urlBeforeVisit, urlAfterVisit) + assert.equal(await visitAction(page), "load") +}) - async getDocumentElementAttribute(attributeName: string): Promise { - return await this.remote.execute( - (attributeName: string) => document.documentElement.getAttribute(attributeName), - [attributeName] +test("test visiting a location served with a non-HTML content type", async ({ page }) => { + const urlBeforeVisit = page.url() + await visitLocation(page, "/src/tests/fixtures/svg.svg") + await nextBeat() + + const url = page.url() + const contentType = await contentTypeOfURL(url) + assert.equal(contentType, "image/svg+xml") + + const urlAfterVisit = page.url() + assert.notEqual(urlBeforeVisit, urlAfterVisit) + assert.equal(await visitAction(page), "load") +}) + +test("test canceling a before-visit event prevents navigation", async ({ page }) => { + await cancelNextVisit(page) + const urlBeforeVisit = page.url() + + assert.notOk( + await willChangeBody(page, async () => { + await page.click("#same-origin-link") + await nextBeat() + }) + ) + + const urlAfterVisit = page.url() + assert.equal(urlAfterVisit, urlBeforeVisit) +}) + +test("test navigation by history is not cancelable", async ({ page }) => { + await page.click("#same-origin-link") + await nextEventNamed(page, "turbo:load") + + assert.equal(await page.textContent("h1"), "One") + + await cancelNextVisit(page) + await page.goBack() + await nextEventNamed(page, "turbo:load") + + assert.equal(await page.textContent("h1"), "Visit") +}) + +test("test turbo:before-fetch-request event.detail", async ({ page }) => { + await page.click("#same-origin-link") + const { url, fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") + + assert.equal(fetchOptions.method, "GET") + assert.ok(url.includes("/src/tests/fixtures/one.html")) +}) + +test("test turbo:before-fetch-request event.detail encodes searchParams", async ({ page }) => { + await page.click("#same-origin-link-search-params") + const { url } = await nextEventNamed(page, "turbo:before-fetch-request") + + assert.ok(url.includes("/src/tests/fixtures/one.html?key=value")) +}) + +test("test turbo:before-fetch-response open new site", async ({ page }) => { + page.evaluate(() => + addEventListener( + "turbo:before-fetch-response", + async function eventListener(event: any) { + removeEventListener("turbo:before-fetch-response", eventListener, false) + ;(window as any).fetchResponseResult = { + responseText: await event.detail.fetchResponse.responseText, + responseHTML: await event.detail.fetchResponse.responseHTML, + } + }, + false ) - } + ) + + await page.click("#sample-response") + await nextEventNamed(page, "turbo:before-fetch-response") + + const fetchResponseResult = await page.evaluate(() => (window as any).fetchResponseResult) + + assert.isTrue(fetchResponseResult.responseText.indexOf("An element with an ID") > -1) + assert.isTrue(fetchResponseResult.responseHTML.indexOf("An element with an ID") > -1) +}) + +test("test cache does not override response after redirect", async ({ page }) => { + await page.evaluate(() => { + const cachedElement = document.createElement("some-cached-element") + document.body.appendChild(cachedElement) + }) + + assert.equal(await page.locator("some-cached-element").count(), 1) + + await page.click("#same-origin-link") + await nextBeat() + await page.click("#redirection-link") + await nextBeat() // 301 redirect response + await nextBeat() // 200 response + + assert.equal(await page.locator("some-cached-element").count(), 0) +}) + +function cancelNextVisit(page: Page): Promise { + return page.evaluate(() => addEventListener("turbo:before-visit", (event) => event.preventDefault(), { once: true })) } function contentTypeOfURL(url: string): Promise { @@ -156,4 +145,6 @@ function contentTypeOfURL(url: string): Promise { }) } -VisitTests.registerSuite() +async function visitLocation(page: Page, location: string) { + return page.evaluate((location) => window.Turbo.visit(location), location) +} diff --git a/src/tests/helpers/functional_test_case.ts b/src/tests/helpers/functional_test_case.ts deleted file mode 100644 index bb93e165f..000000000 --- a/src/tests/helpers/functional_test_case.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { InternTestCase } from "./intern_test_case" -import { Element } from "@theintern/leadfoot" - -export class FunctionalTestCase extends InternTestCase { - get remote() { - return this.internTest.remote - } - - async goToLocation(location: string): Promise { - const processedLocation = location.match(/^\//) ? location.slice(1) : location - return this.remote.get(processedLocation) - } - - async goBack(): Promise { - return this.remote.goBack() - } - - async goForward(): Promise { - return this.remote.goForward() - } - - async reload(): Promise { - await this.evaluate(() => location.reload()) - return this.nextBeat - } - - async hasSelector(selector: string) { - return (await this.remote.findAllByCssSelector(selector)).length > 0 - } - - async selectorHasFocus(selector: string) { - const activeElement = await this.remote.getActiveElement() - - return activeElement.equals(await this.querySelector(selector)) - } - - async querySelector(selector: string) { - return this.remote.findByCssSelector(selector) - } - - async waitUntilSelector(selector: string): Promise { - return (async () => { - let hasSelector = false - do hasSelector = await this.hasSelector(selector) - while (!hasSelector) - })() - } - - async waitUntilNoSelector(selector: string): Promise { - return (async () => { - let hasSelector = true - do hasSelector = await this.hasSelector(selector) - while (hasSelector) - })() - } - - async querySelectorAll(selector: string) { - return this.remote.findAllByCssSelector(selector) - } - - async clickSelector(selector: string): Promise { - return (await this.remote.findByCssSelector(selector)).click() - } - - async scrollToSelector(selector: string): Promise { - const element = await this.remote.findByCssSelector(selector) - return this.evaluate((element) => element.scrollIntoView(), element) - } - - async pressTab(): Promise { - return this.remote.getActiveElement().then((activeElement) => activeElement.type("\uE004")) // TAB - } - - async pressEnter(): Promise { - return this.remote.getActiveElement().then((activeElement) => activeElement.type("\uE006")) // ENTER - } - - async outerHTMLForSelector(selector: string): Promise { - const element = await this.remote.findByCssSelector(selector) - return this.evaluate((element) => element.outerHTML, element) - } - - async innerHTMLForSelector(selector: string): Promise { - const element = await this.remote.findAllByCssSelector(selector) - return this.evaluate((element) => element.innerHTML, element) - } - - async attributeForSelector(selector: string, attributeName: string) { - const element = await this.querySelector(selector) - - return await element.getAttribute(attributeName) - } - - async propertyForSelector(selector: string, attributeName: string) { - const element = await this.querySelector(selector) - - return await element.getProperty(attributeName) - } - - get scrollPosition(): Promise<{ x: number; y: number }> { - return this.evaluate(() => ({ x: window.scrollX, y: window.scrollY })) - } - - async isScrolledToTop(): Promise { - const { y: pageY } = await this.scrollPosition - return pageY === 0 - } - - async isScrolledToSelector(selector: string): Promise { - const { y: pageY } = await this.scrollPosition - const { y: elementY } = await this.remote.findByCssSelector(selector).getPosition() - const offset = pageY - elementY - return Math.abs(offset) < 2 - } - - get nextBeat(): Promise { - return this.sleep(100) - } - - async sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)) - } - - async evaluate(callback: (...args: any[]) => T, ...args: any[]): Promise { - return await this.remote.execute(callback, args) - } - - get head(): Promise { - return this.evaluate(() => document.head as any) - } - - get body(): Promise { - return this.evaluate(() => document.body as any) - } - - get location(): Promise { - return this.evaluate(() => location.toString()) - } - - get origin(): Promise { - return this.evaluate(() => location.origin.toString()) - } - - get pathname(): Promise { - return this.evaluate(() => location.pathname) - } - - get search(): Promise { - return this.evaluate(() => location.search) - } - - get searchParams(): Promise { - return this.evaluate(() => location.search).then((search) => new URLSearchParams(search)) - } - - async getSearchParam(key: string): Promise { - return (await this.searchParams).get(key) || "" - } - - async getAllSearchParams(key: string): Promise { - return (await this.searchParams).getAll(key) || [] - } - - get hash(): Promise { - return this.evaluate(() => location.hash) - } - - async acceptAlert(): Promise { - return this.remote.acceptAlert() - } - - async dismissAlert(): Promise { - return this.remote.dismissAlert() - } - - async getAlertText(): Promise { - return this.remote.getAlertText() - } -} diff --git a/src/tests/helpers/page.ts b/src/tests/helpers/page.ts new file mode 100644 index 000000000..347e3eb80 --- /dev/null +++ b/src/tests/helpers/page.ts @@ -0,0 +1,241 @@ +import { JSHandle, Locator, Page } from "@playwright/test" + +type EventLog = [string, any, string | null] +type MutationLog = [string, string | null, string | null] + +export function attributeForSelector(page: Page, selector: string, attributeName: string): Promise { + return page.locator(selector).getAttribute(attributeName) +} + +export function clickWithoutScrolling(page: Page, selector: string, options = {}) { + const element = page.locator(selector, options) + + return element.evaluate((element) => element instanceof HTMLElement && element.click()) +} + +export function clearLocalStorage(page: Page): Promise { + return page.evaluate(() => localStorage.clear()) +} + +export function disposeAll(...handles: JSHandle[]): Promise { + return Promise.all(handles.map((handle) => handle.dispose())) +} + +export function getFromLocalStorage(page: Page, key: string) { + return page.evaluate((storageKey: string) => localStorage.getItem(storageKey), key) +} + +export function getSearchParam(url: string, key: string): string | null { + return searchParams(url).get(key) +} + +export function hash(url: string): string { + const { hash } = new URL(url) + + return hash +} + +export async function hasSelector(page: Page, selector: string): Promise { + return !!(await page.locator(selector).count()) +} + +export function innerHTMLForSelector(page: Page, selector: string): Promise { + return page.locator(selector).innerHTML() +} + +export async function isScrolledToSelector(page: Page, selector: string): Promise { + const boundingBox = await page + .locator(selector) + .evaluate((element) => (element instanceof HTMLElement ? { x: element.offsetLeft, y: element.offsetTop } : null)) + + if (boundingBox) { + const { y: pageY } = await scrollPosition(page) + const { y: elementY } = boundingBox + const offset = pageY - elementY + return Math.abs(offset) < 2 + } else { + return false + } +} + +export function nextBeat() { + return sleep(100) +} + +export function nextBody(_page: Page, timeout = 500) { + return sleep(timeout) +} + +export async function nextEventNamed(page: Page, eventName: string): Promise { + let record: EventLog | undefined + while (!record) { + const records = await readEventLogs(page, 1) + record = records.find(([name]) => name == eventName) + } + return record[1] +} + +export async function nextEventOnTarget(page: Page, elementId: string, eventName: string): Promise { + let record: EventLog | undefined + while (!record) { + const records = await readEventLogs(page, 1) + record = records.find(([name, _, id]) => name == eventName && id == elementId) + } + return record[1] +} + +export async function nextAttributeMutationNamed( + page: Page, + elementId: string, + attributeName: string +): Promise { + let record: MutationLog | undefined + while (!record) { + const records = await readMutationLogs(page, 1) + record = records.find(([name, id]) => name == attributeName && id == elementId) + } + const attributeValue = record[2] + return attributeValue +} + +export async function noNextEventNamed(page: Page, eventName: string): Promise { + const records = await readEventLogs(page, 1) + return !records.some(([name]) => name == eventName) +} + +export async function noNextEventOnTarget(page: Page, elementId: string, eventName: string): Promise { + const records = await readEventLogs(page, 1) + return !records.some(([name, _, target]) => name == eventName && target == elementId) +} + +export async function outerHTMLForSelector(page: Page, selector: string): Promise { + const element = await page.locator(selector) + return element.evaluate((element) => element.outerHTML) +} + +export function pathname(url: string): string { + const { pathname } = new URL(url) + + return pathname +} + +export function propertyForSelector(page: Page, selector: string, propertyName: string): Promise { + return page.locator(selector).evaluate((element, propertyName) => (element as any)[propertyName], propertyName) +} + +async function readArray(page: Page, identifier: string, length?: number): Promise { + return page.evaluate( + ({ identifier, length }) => { + const records = (window as any)[identifier] + if (records != null && typeof records.splice == "function") { + return records.splice(0, typeof length === "undefined" ? records.length : length) + } else { + return [] + } + }, + { identifier, length } + ) +} + +export function readEventLogs(page: Page, length?: number): Promise { + return readArray(page, "eventLogs", length) +} + +export function readMutationLogs(page: Page, length?: number): Promise { + return readArray(page, "mutationLogs", length) +} + +export function search(url: string): string { + const { search } = new URL(url) + + return search +} + +export function searchParams(url: string): URLSearchParams { + const { searchParams } = new URL(url) + + return searchParams +} + +export function selectorHasFocus(page: Page, selector: string): Promise { + return page.locator(selector).evaluate((element) => element === document.activeElement) +} + +export function setLocalStorageFromEvent(page: Page, eventName: string, storageKey: string, storageValue: string) { + return page.evaluate( + ({ eventName, storageKey, storageValue }) => { + addEventListener(eventName, () => localStorage.setItem(storageKey, storageValue)) + }, + { eventName, storageKey, storageValue } + ) +} + +export function scrollPosition(page: Page): Promise<{ x: number; y: number }> { + return page.evaluate(() => ({ x: window.scrollX, y: window.scrollY })) +} + +export async function isScrolledToTop(page: Page): Promise { + const { y: pageY } = await scrollPosition(page) + return pageY === 0 +} + +export function scrollToSelector(page: Page, selector: string): Promise { + return page.locator(selector).scrollIntoViewIfNeeded() +} + +export function sleep(timeout = 0): Promise { + return new Promise((resolve) => setTimeout(() => resolve(undefined), timeout)) +} + +export async function strictElementEquals(left: Locator, right: Locator): Promise { + return left.evaluate((left, right) => left === right, await right.elementHandle()) +} + +export function textContent(page: Page, html: string): Promise { + return page.evaluate((html) => { + const parser = new DOMParser() + const { documentElement } = parser.parseFromString(html, "text/html") + + return documentElement.textContent + }, html) +} + +export function visitAction(page: Page): Promise { + return page.evaluate(() => { + try { + return window.Turbo.navigator.currentVisit!.action + } catch (error) { + return "load" + } + }) +} + +export function waitForPathname(page: Page, pathname: string): Promise { + return page.waitForURL((url) => url.pathname == pathname) +} + +export function waitUntilSelector(page: Page, selector: string, state: "visible" | "attached" = "visible") { + return page.waitForSelector(selector, { state }) +} + +export function waitUntilNoSelector(page: Page, selector: string, state: "hidden" | "detached" = "hidden") { + return page.waitForSelector(selector, { state }) +} + +export async function willChangeBody(page: Page, callback: () => Promise): Promise { + const handles: JSHandle[] = [] + + try { + const originalBody = await page.evaluateHandle(() => document.body) + handles.push(originalBody) + + await callback() + + const latestBody = await page.evaluateHandle(() => document.body) + handles.push(latestBody) + + return page.evaluate(({ originalBody, latestBody }) => originalBody !== latestBody, { originalBody, latestBody }) + } finally { + disposeAll(...handles) + } +} diff --git a/src/tests/helpers/remote_channel.ts b/src/tests/helpers/remote_channel.ts deleted file mode 100644 index 22e4a17d7..000000000 --- a/src/tests/helpers/remote_channel.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Remote } from "intern/lib/executors/Node" - -export class RemoteChannel { - readonly remote: Remote - readonly identifier: string - private index = 0 - - constructor(remote: Remote, identifier: string) { - this.remote = remote - this.identifier = identifier - } - - async read(length?: number): Promise { - const records = (await this.newRecords).slice(0, length) - this.index += records.length - return records - } - - async drain(): Promise { - await this.read() - } - - private get newRecords() { - return this.remote.execute( - (identifier: string, index: number) => { - const records = (window as any)[identifier] - if (records != null && typeof records.slice == "function") { - return records.slice(index) - } else { - return [] - } - }, - [this.identifier, this.index] - ) - } -} diff --git a/src/tests/helpers/turbo_drive_test_case.ts b/src/tests/helpers/turbo_drive_test_case.ts deleted file mode 100644 index 830e463a6..000000000 --- a/src/tests/helpers/turbo_drive_test_case.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { FunctionalTestCase } from "./functional_test_case" -import { RemoteChannel } from "./remote_channel" -import { Element } from "@theintern/leadfoot" - -type EventLog = [string, any, string | null] -type MutationLog = [string, string | null, string | null] - -export class TurboDriveTestCase extends FunctionalTestCase { - eventLogChannel: RemoteChannel = new RemoteChannel(this.remote, "eventLogs") - mutationLogChannel: RemoteChannel = new RemoteChannel(this.remote, "mutationLogs") - lastBody?: Element - - async beforeTest() { - await this.clearLocalStorage() - await this.drainEventLog() - this.lastBody = await this.body - } - - get nextWindowHandle(): Promise { - return (async (nextHandle?: string) => { - do { - const handle = await this.remote.getCurrentWindowHandle() - const handles = await this.remote.getAllWindowHandles() - nextHandle = handles[handles.indexOf(handle) + 1] - } while (!nextHandle) - return nextHandle - })() - } - - async nextEventNamed(eventName: string): Promise { - let record: EventLog | undefined - while (!record) { - const records = await this.eventLogChannel.read(1) - record = records.find(([name]) => name == eventName) - } - return record[1] - } - - async noNextEventNamed(eventName: string): Promise { - const records = await this.eventLogChannel.read(1) - return !records.some(([name]) => name == eventName) - } - - async nextEventOnTarget(elementId: string, eventName: string): Promise { - let record: EventLog | undefined - while (!record) { - const records = await this.eventLogChannel.read(1) - record = records.find(([name, _, id]) => name == eventName && id == elementId) - } - return record[1] - } - - async nextAttributeMutationNamed(elementId: string, attributeName: string): Promise { - let record: MutationLog | undefined - while (!record) { - const records = await this.mutationLogChannel.read(1) - record = records.find(([name, id]) => name == attributeName && id == elementId) - } - const attributeValue = record[2] - return attributeValue - } - - async setLocalStorageFromEvent(event: string, key: string, value: string) { - return this.remote.execute( - (eventName: string, storageKey: string, storageValue: string) => { - addEventListener(eventName, () => localStorage.setItem(storageKey, storageValue)) - }, - [event, key, value] - ) - } - - getFromLocalStorage(key: string) { - return this.remote.execute((storageKey: string) => localStorage.getItem(storageKey), [key]) - } - - get nextBody(): Promise { - return (async () => { - let body - do body = await this.changedBody - while (!body) - return (this.lastBody = body) - })() - } - - get changedBody(): Promise { - return (async () => { - const body = await this.body - if (!this.lastBody || this.lastBody.elementId != body.elementId) { - return body - } - })() - } - - get visitAction(): Promise { - return this.evaluate(() => { - try { - return window.Turbo.navigator.currentVisit!.action - } catch (error) { - return "load" - } - }) - } - - drainEventLog() { - return this.eventLogChannel.drain() - } - - clearLocalStorage() { - this.remote.execute(() => localStorage.clear()) - } -} diff --git a/src/tests/runner.js b/src/tests/runner.js index 7672b9d60..0acc08560 100644 --- a/src/tests/runner.js +++ b/src/tests/runner.js @@ -33,14 +33,6 @@ if (args["--environment"]) { const firstArg = args["_"][0] if (firstArg == "serveOnly") { intern.configure({ serveOnly: true }) -} else { - const { spawnSync } = require("child_process") - const { status, stderr } = spawnSync("java", [ "-version" ]) - - if (status != 0) { - console.error(stderr.toString()) - process.exit(status) - } } intern.on("serverStart", server => { diff --git a/tsconfig.json b/tsconfig.json index dc422ad2d..6b133cf60 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,5 @@ "removeComments": true, "skipLibCheck": true, }, - "exclude": [ "dist", "src/tests/fixtures" ] + "exclude": [ "dist", "src/tests/fixtures", "playwright.config.ts" ] } diff --git a/yarn.lock b/yarn.lock index 270176402..efffcfc0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,95 +2,185 @@ # yarn lockfile v1 -"@babel/code-frame@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz" - integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== - dependencies: - "@babel/highlight" "^7.10.4" +"@ampproject/remapping@^2.1.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" + integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== + dependencies: + "@jridgewell/gen-mapping" "^0.1.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" + integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== + dependencies: + "@babel/highlight" "^7.16.7" + +"@babel/compat-data@^7.17.10": + version "7.18.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.5.tgz#acac0c839e317038c73137fbb6ef71a1d6238471" + integrity sha512-BxhE40PVCBxVEJsSBhB6UWyAuqJRxGsAw8BdHMJ3AKGydcwuWW4kOO3HmqBQAdcq/OP+/DlTVxLvsCzRTnZuGg== + +"@babel/core@^7.7.5": + version "7.18.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.5.tgz#c597fa680e58d571c28dda9827669c78cdd7f000" + integrity sha512-MGY8vg3DxMnctw0LdvSEojOsumc70g0t18gNyUdAZqB1Rpd1Bqo/svHGvt+UJ6JcGX+DIekGFDxxIWofBxLCnQ== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.18.2" + "@babel/helper-compilation-targets" "^7.18.2" + "@babel/helper-module-transforms" "^7.18.0" + "@babel/helpers" "^7.18.2" + "@babel/parser" "^7.18.5" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.18.5" + "@babel/types" "^7.18.4" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" -"@babel/generator@^7.12.5", "@babel/generator@^7.4.0": - version "7.12.5" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz" - integrity sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A== +"@babel/generator@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.2.tgz#33873d6f89b21efe2da63fe554460f3df1c5880d" + integrity sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw== dependencies: - "@babel/types" "^7.12.5" + "@babel/types" "^7.18.2" + "@jridgewell/gen-mapping" "^0.3.0" jsesc "^2.5.1" - source-map "^0.5.0" - -"@babel/helper-function-name@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz" - integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== - dependencies: - "@babel/helper-get-function-arity" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-get-function-arity@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz" - integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== - dependencies: - "@babel/types" "^7.10.4" -"@babel/helper-split-export-declaration@^7.11.0": - version "7.11.0" - resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz" - integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== - dependencies: - "@babel/types" "^7.11.0" - -"@babel/helper-validator-identifier@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz" - integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== - -"@babel/highlight@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz" - integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" +"@babel/helper-compilation-targets@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.2.tgz#67a85a10cbd5fc7f1457fec2e7f45441dc6c754b" + integrity sha512-s1jnPotJS9uQnzFtiZVBUxe67CuBa679oWFHpxYYnTpRL/1ffhyX44R9uYiXoa/pLXcY9H2moJta0iaanlk/rQ== + dependencies: + "@babel/compat-data" "^7.17.10" + "@babel/helper-validator-option" "^7.16.7" + browserslist "^4.20.2" + semver "^6.3.0" + +"@babel/helper-environment-visitor@^7.16.7", "@babel/helper-environment-visitor@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.2.tgz#8a6d2dedb53f6bf248e31b4baf38739ee4a637bd" + integrity sha512-14GQKWkX9oJzPiQQ7/J36FTXcD4kSp8egKjO9nINlSKiHITRA9q/R74qu8S9xlc/b/yjsJItQUeeh3xnGN0voQ== + +"@babel/helper-function-name@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz#136fcd54bc1da82fcb47565cf16fd8e444b1ff12" + integrity sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg== + dependencies: + "@babel/template" "^7.16.7" + "@babel/types" "^7.17.0" + +"@babel/helper-hoist-variables@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" + integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-module-imports@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" + integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-module-transforms@^7.18.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.0.tgz#baf05dec7a5875fb9235bd34ca18bad4e21221cd" + integrity sha512-kclUYSUBIjlvnzN2++K9f2qzYKFgjmnmjwL4zlmU5f8ZtzgWe8s0rUPSTGy2HmK4P8T52MQsS+HTQAgZd3dMEA== + dependencies: + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-simple-access" "^7.17.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.18.0" + "@babel/types" "^7.18.0" + +"@babel/helper-simple-access@^7.17.7": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.2.tgz#4dc473c2169ac3a1c9f4a51cfcd091d1c36fcff9" + integrity sha512-7LIrjYzndorDY88MycupkpQLKS1AFfsVRm2k/9PtKScSy5tZq0McZTj+DiMRynboZfIqOKvo03pmhTaUgiD6fQ== + dependencies: + "@babel/types" "^7.18.2" + +"@babel/helper-split-export-declaration@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b" + integrity sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== + +"@babel/helper-validator-option@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" + integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== + +"@babel/helpers@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.2.tgz#970d74f0deadc3f5a938bfa250738eb4ac889384" + integrity sha512-j+d+u5xT5utcQSzrh9p+PaJX94h++KN+ng9b9WEJq7pkUPAd61FGqhjuUEdfknb3E/uDBb7ruwEeKkIxNJPIrg== + dependencies: + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.18.2" + "@babel/types" "^7.18.2" + +"@babel/highlight@^7.16.7": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.17.12.tgz#257de56ee5afbd20451ac0a75686b6b404257351" + integrity sha512-7yykMVF3hfZY2jsHZEEgLc+3x4o1O+fYyULu11GynEUQNwB6lua+IIQn1FiJxNucd5UlyJryrwsOh8PL9Sn8Qg== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.12.7", "@babel/parser@^7.4.3": - version "7.12.7" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz" - integrity sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg== - -"@babel/template@^7.10.4", "@babel/template@^7.4.0": - version "7.12.7" - resolved "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz" - integrity sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/parser" "^7.12.7" - "@babel/types" "^7.12.7" - -"@babel/traverse@^7.4.3": - version "7.12.9" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.9.tgz" - integrity sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.12.5" - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/parser" "^7.12.7" - "@babel/types" "^7.12.7" +"@babel/parser@^7.16.7", "@babel/parser@^7.18.5": + version "7.18.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.5.tgz#337062363436a893a2d22faa60be5bb37091c83c" + integrity sha512-YZWVaglMiplo7v8f1oMQ5ZPQr0vn7HPeZXxXWsxXJRjGVrzUFn9OxFQl1sb5wzfootjA/yChhW84BV+383FSOw== + +"@babel/template@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" + integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/parser" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/traverse@^7.18.0", "@babel/traverse@^7.18.2", "@babel/traverse@^7.18.5": + version "7.18.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.5.tgz#94a8195ad9642801837988ab77f36e992d9a20cd" + integrity sha512-aKXj1KT66sBj0vVzk6rEeAO6Z9aiiQ68wfDgge3nHhA/my6xMM/7HGQUNumKZaoa2qUPQ5whJG9aAifsxUKfLA== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.18.2" + "@babel/helper-environment-visitor" "^7.18.2" + "@babel/helper-function-name" "^7.17.9" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.18.5" + "@babel/types" "^7.18.4" debug "^4.1.0" globals "^11.1.0" - lodash "^4.17.19" -"@babel/types@^7.10.4", "@babel/types@^7.11.0", "@babel/types@^7.12.5", "@babel/types@^7.12.7", "@babel/types@^7.4.0": - version "7.12.7" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz" - integrity sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ== +"@babel/types@^7.16.7", "@babel/types@^7.17.0", "@babel/types@^7.18.0", "@babel/types@^7.18.2", "@babel/types@^7.18.4": + version "7.18.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.4.tgz#27eae9b9fd18e9dccc3f9d6ad051336f307be354" + integrity sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw== dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - lodash "^4.17.19" + "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" "@eslint/eslintrc@^1.2.1": @@ -122,6 +212,51 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jridgewell/gen-mapping@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" + integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz#cf92a983c83466b8c0ce9124fadeaf09f7c66ea9" + integrity sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz#30cd49820a962aff48c8fffc5cd760151fca61fe" + integrity sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA== + +"@jridgewell/set-array@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.1.tgz#36a6acc93987adcf0ba50c66908bd0b70de8afea" + integrity sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.13" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c" + integrity sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w== + +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz#dcfe3e95f224c8fe97a87a5235defec999aa92ea" + integrity sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -143,6 +278,14 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@playwright/test@^1.22.2": + version "1.22.2" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.22.2.tgz#b848f25f8918140c2d0bae8e9227a40198f2dd4a" + integrity sha512-cCl96BEBGPtptFz7C2FOSN3PrTnJ3rPpENe+gYCMx4GNNDlN4tmo2D89y13feGKTMMAIVrXfSQ/UmaQKLy1XLA== + dependencies: + "@types/node" "*" + playwright-core "1.22.2" + "@rollup/plugin-node-resolve@13.1.3": version "13.1.3" resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.1.3.tgz" @@ -172,42 +315,62 @@ estree-walker "^1.0.1" picomatch "^2.2.2" -"@theintern/common@~0.2.3": - version "0.2.3" - resolved "https://registry.npmjs.org/@theintern/common/-/common-0.2.3.tgz" - integrity sha512-91kL3C6USiNfumAm5m07HjGdc40IJaNzEczhcdW5T8fjsHVNg8ttVSXCzy5C9BM57WLevVjR5eHUNjEl4foGMQ== +"@theintern/common@~0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@theintern/common/-/common-0.3.0.tgz#a8351b9ab815fa8b0d846e5373b626994a6e80ad" + integrity sha512-VKSyZGEyzmicJPvV5Gxeavm8Xbcr0cETAAqMapWZzA9Q85YHMG8VSrmPFlMrDQ524qE0IqQsTi0IlH8NIaN+eQ== dependencies: - axios "~0.19.0" - tslib "~1.9.3" + axios "~0.21.1" + tslib "~2.3.0" -"@theintern/digdug@~2.5.0": - version "2.5.0" - resolved "https://registry.npmjs.org/@theintern/digdug/-/digdug-2.5.0.tgz" - integrity sha512-g5mRt94GENnXxHgpccK9gjwyaK+61+fnF+njMnJGJQkxhjCjfShu9R3btt3/vSy5kkWxop83UN2/oAkiyqTKDw== +"@theintern/digdug@~2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@theintern/digdug/-/digdug-2.6.2.tgz#c03fab97cff3128108823d2eb2924bdf63a06a69" + integrity sha512-r9P7zkIp8L2LYKOUfcKl+KOHUTWrIZ9X6Efsb7Tn+OtiIv4oRlXorcoj/5vmrRLO5JF8jFj26HyeSWBNQA2uwg== dependencies: - "@theintern/common" "~0.2.3" - command-exists "~1.2.6" - decompress "~4.2.0" - tslib "~1.9.3" + "@theintern/common" "~0.3.0" + command-exists "~1.2.9" + decompress "~4.2.1" + tslib "~2.3.0" -"@theintern/leadfoot@~2.3.2": - version "2.3.2" - resolved "https://registry.npmjs.org/@theintern/leadfoot/-/leadfoot-2.3.2.tgz" - integrity sha512-NskDofysJMJad5uEYUc7Y3AlP3IdhY3t+H6XyiTDPur/p4pzVvKGGoCygE5FsN/K26i0XOmhNEIOwJtOMh47Hg== +"@theintern/leadfoot@~2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@theintern/leadfoot/-/leadfoot-2.4.1.tgz#4f8f69d968503e5b8488c17d39e35be2cae4d10f" + integrity sha512-WnmmMlSROXQc6sGJdQCcSXYbrRAni2HMmjjr2qtvXtLNCi7ZG6O/H7rJ+1fNdJckjE3kwF+Ag3Bh1WR7GkfG0Q== dependencies: - "@theintern/common" "~0.2.3" - jszip "~3.2.1" - tslib "~1.9.3" + "@theintern/common" "~0.3.0" + jszip "~3.7.1" + tslib "~2.3.0" + +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.1": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== "@types/babel-types@*": - version "7.0.9" - resolved "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.9.tgz" - integrity sha512-qZLoYeXSTgQuK1h7QQS16hqLGdmqtRmN8w/rl3Au/l5x/zkHx+a4VHrHyBsi1I1vtK2oBHxSzKIu0R5p6spdOA== + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.11.tgz#263b113fa396fac4373188d73225297fb86f19a9" + integrity sha512-pkPtJUUY+Vwv6B1inAz55rQvivClHJxc9aVEPPmaq2cbyeMLCiDpbKpcKyX4LAwpNGi+SHBv0tHv6+0gXv0P2A== -"@types/benchmark@1.0.31": - version "1.0.31" - resolved "https://registry.npmjs.org/@types/benchmark/-/benchmark-1.0.31.tgz" - integrity sha512-F6fVNOkGEkSdo/19yWYOwVKGvzbTeWkR/XQYBKtGBQ9oGRjBN9f/L4aJI4sDcVPJO58Y1CJZN8va9V2BhrZapA== +"@types/benchmark@~2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@types/benchmark/-/benchmark-2.1.1.tgz#d763df29717d93aa333eb11f421ef383a5df5673" + integrity sha512-XmdNOarpSSxnb3DE2rRFOFsEyoqXLUL+7H8nSGS25vs+JS0018bd+cW5Ma9vdlkPmoTHSQ6e8EUFMFMxeE4l+g== "@types/body-parser@*": version "1.19.0" @@ -217,15 +380,15 @@ "@types/connect" "*" "@types/node" "*" -"@types/chai@4.1.7": - version "4.1.7" - resolved "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz" - integrity sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA== +"@types/chai@~4.2.20": + version "4.2.22" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.22.tgz#47020d7e4cf19194d43b5202f35f75bd2ad35ce7" + integrity sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ== -"@types/charm@1.0.1": - version "1.0.1" - resolved "https://registry.npmjs.org/@types/charm/-/charm-1.0.1.tgz" - integrity sha512-F9OalGhk60p/DnACfa1SWtmVTMni0+w9t/qfb5Bu7CsurkEjZFN7Z+ii/VGmYpaViPz7o3tBahRQae9O7skFlQ== +"@types/charm@~1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/charm/-/charm-1.0.3.tgz#1dc44bcbf0a90ef4b6826094fb324796d229d502" + integrity sha512-FpNoSOkloETr+ZJ0RsZpB+a/tqJkniIN+9Enn6uPIbhiNptOWtZzV7FkaqxTRjvvlHeUKMR331Wj9tOmqG10TA== dependencies: "@types/node" "*" @@ -241,11 +404,6 @@ resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/events@*": - version "3.0.0" - resolved "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz" - integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== - "@types/express-serve-static-core@*": version "4.17.14" resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.14.tgz" @@ -255,7 +413,16 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@*", "@types/express@~4.17.0": +"@types/express-serve-static-core@^4.17.18": + version "4.17.29" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz#2a1795ea8e9e9c91b4a4bbe475034b20c1ec711c" + integrity sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@*": version "4.17.9" resolved "https://registry.npmjs.org/@types/express/-/express-4.17.9.tgz" integrity sha512-SDzEIZInC4sivGIFY4Sz1GG6J9UObPwCInYJjko2jzOf/Imx/dlpume6Xxwj1ORL82tBbmN4cPDIDkLbWHk9hw== @@ -265,41 +432,50 @@ "@types/qs" "*" "@types/serve-static" "*" -"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@~2.0.1": - version "2.0.3" - resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz" - integrity sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw== +"@types/express@~4.17.13": + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" + integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.18" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@~2.0.3": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== -"@types/istanbul-lib-instrument@~1.7.3": +"@types/istanbul-lib-instrument@~1.7.4": version "1.7.4" - resolved "https://registry.npmjs.org/@types/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.4.tgz" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.4.tgz#474503169db59ada532dd863885c67b217ab67f1" integrity sha512-1i1VVkU2KrpZCmti+t5J/zBb2KLKxHgU1EYL+0QtnDnVyZ59aSKcpnG6J0I6BZGDON566YzPNIlNfk7m+9l1JA== dependencies: "@types/babel-types" "*" "@types/istanbul-lib-coverage" "*" source-map "^0.6.1" -"@types/istanbul-lib-report@*", "@types/istanbul-lib-report@~1.1.1": - version "1.1.1" - resolved "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz" - integrity sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg== +"@types/istanbul-lib-report@*", "@types/istanbul-lib-report@~3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== dependencies: "@types/istanbul-lib-coverage" "*" -"@types/istanbul-lib-source-maps@~1.2.2": - version "1.2.2" - resolved "https://registry.npmjs.org/@types/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.2.tgz" - integrity sha512-41eeNQ3Du3++LV0Hdz7m0UbeYMnShlJ7CkUOVy3tBeFwc0BE7chBs2Vqdx7xOzXBo2iRQfyiWBmqIZTbau3q+A== +"@types/istanbul-lib-source-maps@~4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#8acb1f6230bf9d732e9fc30590e5ccaabbefec7b" + integrity sha512-WH6e5naLXI3vB2Px3whNeYxzDgm6S6sk3Ht8e3/BiWwEnzZi72wja3bWzWwcgbFTFp8hBLB7NT2p3lNJgxCxvA== dependencies: "@types/istanbul-lib-coverage" "*" source-map "^0.6.1" -"@types/istanbul-reports@~1.1.1": - version "1.1.2" - resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz" - integrity sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw== +"@types/istanbul-reports@~3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== dependencies: - "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" "@types/json-schema@^7.0.9": @@ -349,12 +525,11 @@ "@types/mime" "*" "@types/node" "*" -"@types/ws@6.0.1": - version "6.0.1" - resolved "https://registry.npmjs.org/@types/ws/-/ws-6.0.1.tgz" - integrity sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q== +"@types/ws@7.4.6": + version "7.4.6" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.6.tgz#c4320845e43d45a7129bb32905e28781c71c1fff" + integrity sha512-ijZ1vzRawI7QoWnTNL8KpHixd2b2XVb9I9HAqI3triPsh1EC0xH0Eg6w2O3TKbDCgiNNlJqfrof6j4T2I+l9vw== dependencies: - "@types/events" "*" "@types/node" "*" "@typescript-eslint/eslint-plugin@^5.20.0": @@ -437,13 +612,13 @@ "@typescript-eslint/types" "5.20.0" eslint-visitor-keys "^3.0.0" -accepts@~1.3.7: - version "1.3.7" - resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" + mime-types "~2.1.34" + negotiator "0.6.3" acorn-jsx@^5.3.1: version "5.3.2" @@ -472,7 +647,7 @@ ansi-regex@^5.0.1: ansi-styles@^3.2.1: version "3.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" @@ -489,16 +664,16 @@ append-field@^1.0.0: resolved "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz" integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY= -append-transform@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz" - integrity sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw== +append-transform@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-2.0.0.tgz#99d9d29c7b38391e6f428d28ce136551f0b77e12" + integrity sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg== dependencies: - default-require-extensions "^2.0.0" + default-require-extensions "^3.0.0" arg@^4.1.0: version "4.1.3" - resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== arg@^5.0.1: @@ -513,8 +688,8 @@ argparse@^2.0.1: array-flatten@1.1.1: version "1.1.1" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== array-union@^2.1.0: version "2.1.0" @@ -523,20 +698,15 @@ array-union@^2.1.0: assertion-error@^1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== -async-limiter@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== - -axios@~0.19.0: - version "0.19.2" - resolved "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz" - integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== +axios@~0.21.1: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== dependencies: - follow-redirects "1.5.10" + follow-redirects "^1.14.0" balanced-match@^1.0.0: version "1.0.0" @@ -545,40 +715,40 @@ balanced-match@^1.0.0: base64-js@^1.3.1: version "1.5.1" - resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== benchmark@~2.1.4: version "2.1.4" - resolved "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz" - integrity sha1-CfPeMckWQl1JjMLuVloOvzwqVik= + resolved "https://registry.yarnpkg.com/benchmark/-/benchmark-2.1.4.tgz#09f3de31c916425d498cc2ee565a0ebf3c2a5629" + integrity sha512-l9MlfN4M1K/H2fbhfMy3B7vJd6AGKJVQn2h6Sg/Yx+KckoUA7ewS5Vv6TjSq18ooE1kS9hhAlQRH3AkXIh/aOQ== dependencies: lodash "^4.17.4" platform "^1.3.3" bl@^1.0.0: version "1.2.3" - resolved "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7" integrity sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww== dependencies: readable-stream "^2.3.5" safe-buffer "^5.1.1" -body-parser@1.19.0, body-parser@~1.19.0: - version "1.19.0" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz" - integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== +body-parser@1.19.2, body-parser@~1.19.0: + version "1.19.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.2.tgz#4714ccd9c157d44797b8b5607d72c0b89952f26e" + integrity sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw== dependencies: - bytes "3.1.0" + bytes "3.1.2" content-type "~1.0.4" debug "2.6.9" depd "~1.1.2" - http-errors "1.7.2" + http-errors "1.8.1" iconv-lite "0.4.24" on-finished "~2.3.0" - qs "6.7.0" - raw-body "2.4.0" - type-is "~1.6.17" + qs "6.9.7" + raw-body "2.4.3" + type-is "~1.6.18" brace-expansion@^1.1.7: version "1.1.11" @@ -595,14 +765,25 @@ braces@^3.0.2: dependencies: fill-range "^7.0.1" +browserslist@^4.20.2: + version "4.20.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.4.tgz#98096c9042af689ee1e0271333dbc564b8ce4477" + integrity sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw== + dependencies: + caniuse-lite "^1.0.30001349" + electron-to-chromium "^1.4.147" + escalade "^3.1.1" + node-releases "^2.0.5" + picocolors "^1.0.0" + buffer-alloc-unsafe@^1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== buffer-alloc@^1.2.0: version "1.2.0" - resolved "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== dependencies: buffer-alloc-unsafe "^1.1.0" @@ -610,13 +791,13 @@ buffer-alloc@^1.2.0: buffer-crc32@~0.2.3: version "0.2.13" - resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz" - integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== buffer-fill@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz" - integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" + integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ== buffer-from@^1.0.0: version "1.1.1" @@ -625,7 +806,7 @@ buffer-from@^1.0.0: buffer@^5.2.1: version "5.7.1" - resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== dependencies: base64-js "^1.3.1" @@ -644,31 +825,37 @@ busboy@^0.2.11: dicer "0.2.5" readable-stream "1.1.x" -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -chai@~4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz" - integrity sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw== +caniuse-lite@^1.0.30001349: + version "1.0.30001357" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001357.tgz#dec7fc4158ef6ad24690d0eec7b91f32b8cb1b5d" + integrity sha512-b+KbWHdHePp+ZpNj+RDHFChZmuN+J5EvuQUlee9jOQIUAdhv9uvAZeEtUeLAknXbkiu1uxjQ9NLp1ie894CuWg== + +chai@~4.3.4: + version "4.3.6" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.6.tgz#ffe4ba2d9fa9d6680cc0b370adae709ec9011e9c" + integrity sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q== dependencies: assertion-error "^1.1.0" check-error "^1.0.2" deep-eql "^3.0.1" get-func-name "^2.0.0" - pathval "^1.1.0" + loupe "^2.3.1" + pathval "^1.1.1" type-detect "^4.0.5" chalk@^2.0.0: version "2.4.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== dependencies: ansi-styles "^3.2.1" @@ -685,19 +872,19 @@ chalk@^4.0.0: charm@~1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/charm/-/charm-1.0.2.tgz" - integrity sha1-it02cVOm2aWBMxBSxAkJkdqZXjU= + resolved "https://registry.yarnpkg.com/charm/-/charm-1.0.2.tgz#8add367153a6d9a581331052c4090991da995e35" + integrity sha512-wqW3VdPnlSWT4eRiYX+hcs+C6ViBPUWk1qTCd+37qw9kEm/a5n2qcyQDMBWvSYKN/ctqZzeXNQaeBjOetJJUkw== dependencies: inherits "^2.0.1" check-error@^1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz" - integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" + integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA== color-convert@^1.9.0: version "1.9.3" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== dependencies: color-name "1.1.3" @@ -711,22 +898,22 @@ color-convert@^2.0.1: color-name@1.1.3: version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -command-exists@~1.2.6: +command-exists@~1.2.9: version "1.2.9" - resolved "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz" + resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69" integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w== commander@^2.8.1: version "2.20.3" - resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== concat-map@0.0.1: @@ -746,36 +933,48 @@ concat-stream@^1.5.2: concurrent@~0.3.2: version "0.3.2" - resolved "https://registry.npmjs.org/concurrent/-/concurrent-0.3.2.tgz" - integrity sha1-DqoAEaFXmMVjURKPIiR/biMX9Q4= + resolved "https://registry.yarnpkg.com/concurrent/-/concurrent-0.3.2.tgz#0eaa0011a15798c56351128f22247f6e2317f50e" + integrity sha512-KoUIH3pHceLMOeviiAnOzdQ8630lNclszDv8IGXx2Gn+5xXZroLqSWWzisweX//X7LyYOCKy10398bb0ksjvsA== -content-disposition@0.5.3: - version "0.5.3" - resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== dependencies: - safe-buffer "5.1.2" + safe-buffer "5.2.1" content-type@~1.0.4: version "1.0.4" - resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +convert-source-map@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + cookie-signature@1.0.6: version "1.0.6" - resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" - integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz" - integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +cookie@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -787,35 +986,28 @@ cross-spawn@^7.0.2: debug@2.6.9: version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@=3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== +debug@^4.1.0, debug@^4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: - ms "2.0.0" + ms "2.1.2" -debug@^4.1.0, debug@^4.1.1: +debug@^4.1.1: version "4.3.1" resolved "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz" integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== dependencies: ms "2.1.2" -debug@^4.3.2: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: version "4.1.1" - resolved "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz" + resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1" integrity sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ== dependencies: file-type "^5.2.0" @@ -824,7 +1016,7 @@ decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: decompress-tarbz2@^4.0.0: version "4.1.1" - resolved "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz" + resolved "https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b" integrity sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A== dependencies: decompress-tar "^4.1.0" @@ -835,7 +1027,7 @@ decompress-tarbz2@^4.0.0: decompress-targz@^4.0.0: version "4.1.1" - resolved "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz" + resolved "https://registry.yarnpkg.com/decompress-targz/-/decompress-targz-4.1.1.tgz#c09bc35c4d11f3de09f2d2da53e9de23e7ce1eee" integrity sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w== dependencies: decompress-tar "^4.1.1" @@ -844,17 +1036,17 @@ decompress-targz@^4.0.0: decompress-unzip@^4.0.1: version "4.0.1" - resolved "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz" - integrity sha1-3qrM39FK6vhVePczroIQ+bSEj2k= + resolved "https://registry.yarnpkg.com/decompress-unzip/-/decompress-unzip-4.0.1.tgz#deaaccdfd14aeaf85578f733ae8210f9b4848f69" + integrity sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw== dependencies: file-type "^3.8.0" get-stream "^2.2.0" pify "^2.3.0" yauzl "^2.4.2" -decompress@~4.2.0: +decompress@~4.2.1: version "4.2.1" - resolved "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz" + resolved "https://registry.yarnpkg.com/decompress/-/decompress-4.2.1.tgz#007f55cc6a62c055afa37c07eb6a4ee1b773f118" integrity sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ== dependencies: decompress-tar "^4.0.0" @@ -868,7 +1060,7 @@ decompress@~4.2.0: deep-eql@^3.0.1: version "3.0.1" - resolved "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw== dependencies: type-detect "^4.0.0" @@ -883,22 +1075,22 @@ deepmerge@^4.2.2: resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== -default-require-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz" - integrity sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc= +default-require-extensions@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-3.0.0.tgz#e03f93aac9b2b6443fc52e5e4a37b3ad9ad8df96" + integrity sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg== dependencies: - strip-bom "^3.0.0" + strip-bom "^4.0.0" depd@~1.1.2: version "1.1.2" - resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== destroy@~1.0.4: version "1.0.4" - resolved "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg== dicer@0.2.5: version "0.2.5" @@ -908,11 +1100,16 @@ dicer@0.2.5: readable-stream "1.1.x" streamsearch "0.1.2" -diff@^4.0.1, diff@~4.0.1: +diff@^4.0.1: version "4.0.2" - resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +diff@~5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -932,27 +1129,37 @@ ee-first@1.1.1: resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= +electron-to-chromium@^1.4.147: + version "1.4.161" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.161.tgz#49cb5b35385bfee6cc439d0a04fbba7a7a7f08a1" + integrity sha512-sTjBRhqh6wFodzZtc5Iu8/R95OkwaPNn7tj/TaDU5nu/5EFiQDtADGAXdR4tJcTEHlYfJpHqigzJqHvPgehP8A== + encodeurl@~1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" - integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== end-of-stream@^1.0.0: version "1.4.4" - resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + escape-html@~1.0.3: version "1.0.3" - resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" - integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== escape-string-regexp@^1.0.5: version "1.0.5" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== escape-string-regexp@^4.0.0: version "4.0.0" @@ -1090,20 +1297,20 @@ esutils@^2.0.2: etag@~1.8.1: version "1.8.1" - resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" - integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== express@~4.17.1: - version "4.17.1" - resolved "https://registry.npmjs.org/express/-/express-4.17.1.tgz" - integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + version "4.17.3" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1" + integrity sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg== dependencies: - accepts "~1.3.7" + accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.19.0" - content-disposition "0.5.3" + body-parser "1.19.2" + content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.4.0" + cookie "0.4.2" cookie-signature "1.0.6" debug "2.6.9" depd "~1.1.2" @@ -1117,13 +1324,13 @@ express@~4.17.1: on-finished "~2.3.0" parseurl "~1.3.3" path-to-regexp "0.1.7" - proxy-addr "~2.0.5" - qs "6.7.0" + proxy-addr "~2.0.7" + qs "6.9.7" range-parser "~1.2.1" - safe-buffer "5.1.2" - send "0.17.1" - serve-static "1.14.1" - setprototypeof "1.1.1" + safe-buffer "5.2.1" + send "0.17.2" + serve-static "1.14.2" + setprototypeof "1.2.0" statuses "~1.5.0" type-is "~1.6.18" utils-merge "1.0.1" @@ -1169,8 +1376,8 @@ fastq@^1.6.0: fd-slicer@~1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz" - integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== dependencies: pend "~1.2.0" @@ -1183,17 +1390,17 @@ file-entry-cache@^6.0.1: file-type@^3.8.0: version "3.9.0" - resolved "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz" - integrity sha1-JXoHg4TR24CHvESdEH1SpSZyuek= + resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" + integrity sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA== file-type@^5.2.0: version "5.2.0" - resolved "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz" - integrity sha1-LdvqfHP/42No365J3DOMBYwritY= + resolved "https://registry.yarnpkg.com/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6" + integrity sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ== file-type@^6.1.0: version "6.2.0" - resolved "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919" integrity sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg== fill-range@^7.0.1: @@ -1205,7 +1412,7 @@ fill-range@^7.0.1: finalhandler@~1.1.2: version "1.1.2" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== dependencies: debug "2.6.9" @@ -1229,26 +1436,24 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== -follow-redirects@1.5.10: - version "1.5.10" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz" - integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== - dependencies: - debug "=3.1.0" +follow-redirects@^1.14.0: + version "1.15.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" + integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== fresh@0.5.2: version "0.5.2" - resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" - integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== fs-constants@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== fs.realpath@^1.0.0: @@ -1271,15 +1476,20 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + get-func-name@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz" - integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" + integrity sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig== get-stream@^2.2.0: version "2.3.1" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz" - integrity sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4= + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de" + integrity sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA== dependencies: object-assign "^4.0.1" pinkie-promise "^2.0.0" @@ -1298,7 +1508,7 @@ glob-parent@^6.0.1: dependencies: is-glob "^4.0.3" -glob@^7.1.3, glob@~7.1.4: +glob@^7.1.3: version "7.1.6" resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -1310,9 +1520,21 @@ glob@^7.1.3, glob@~7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" +glob@~7.1.7: + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + globals@^11.1.0: version "11.12.0" - resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.6.0, globals@^13.9.0: @@ -1335,25 +1557,14 @@ globby@^11.0.4: slash "^3.0.0" graceful-fs@^4.1.10: - version "4.2.4" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== - -handlebars@~4.5.3: - version "4.5.3" - resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz" - integrity sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA== - dependencies: - neo-async "^2.6.0" - optimist "^0.6.1" - source-map "^0.6.1" - optionalDependencies: - uglify-js "^3.1.4" + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== has-flag@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== has-flag@^4.0.0: version "4.0.0" @@ -1369,41 +1580,30 @@ has@^1.0.3: html-escaper@^2.0.0: version "2.0.2" - resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -http-errors@1.7.2: - version "1.7.2" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz" - integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-errors@~1.7.2: - version "1.7.3" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== +http-errors@1.8.1, http-errors@~1.8.0: + version "1.8.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" + integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== dependencies: depd "~1.1.2" inherits "2.0.4" - setprototypeof "1.1.1" + setprototypeof "1.2.0" statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" + toidentifier "1.0.1" iconv-lite@0.4.24: version "0.4.24" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" ieee754@^1.1.13: version "1.2.1" - resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== ignore@^5.1.8, ignore@^5.2.0: @@ -1413,8 +1613,8 @@ ignore@^5.1.8, ignore@^5.2.0: immediate@~3.0.5: version "3.0.6" - resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz" - integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" @@ -1442,59 +1642,53 @@ inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, i resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - intern@^4.9.0: - version "4.9.0" - resolved "https://registry.npmjs.org/intern/-/intern-4.9.0.tgz" - integrity sha512-YxXmvizFf41tY1vjYjgnYknI2eDFHEhvtW8YrF3jwHTdBtDL0vcVBIIozmPBf28Ib7WsSLnzh5auqhzBkSBSRw== - dependencies: - "@theintern/common" "~0.2.3" - "@theintern/digdug" "~2.5.0" - "@theintern/leadfoot" "~2.3.2" - "@types/benchmark" "1.0.31" - "@types/chai" "4.1.7" - "@types/charm" "1.0.1" - "@types/express" "~4.17.0" - "@types/istanbul-lib-coverage" "~2.0.1" - "@types/istanbul-lib-instrument" "~1.7.3" - "@types/istanbul-lib-report" "~1.1.1" - "@types/istanbul-lib-source-maps" "~1.2.2" - "@types/istanbul-reports" "~1.1.1" - "@types/ws" "6.0.1" + version "4.10.1" + resolved "https://registry.yarnpkg.com/intern/-/intern-4.10.1.tgz#4dfba51d70d8c4eaf795f1006b5aeb4d6bef1747" + integrity sha512-GyUmdpdKGoEu1hRMNYeldPF11lFZlC1Pbq28ImzEY+7OHRDinMU9c8jwGxY7eAaUe15oy0Y7cocdjC/mzUuOng== + dependencies: + "@theintern/common" "~0.3.0" + "@theintern/digdug" "~2.6.2" + "@theintern/leadfoot" "~2.4.1" + "@types/benchmark" "~2.1.1" + "@types/chai" "~4.2.20" + "@types/charm" "~1.0.2" + "@types/express" "~4.17.13" + "@types/istanbul-lib-coverage" "~2.0.3" + "@types/istanbul-lib-instrument" "~1.7.4" + "@types/istanbul-lib-report" "~3.0.0" + "@types/istanbul-lib-source-maps" "~4.0.1" + "@types/istanbul-reports" "~3.0.1" + "@types/ws" "7.4.6" benchmark "~2.1.4" body-parser "~1.19.0" - chai "~4.2.0" + chai "~4.3.4" charm "~1.0.2" concurrent "~0.3.2" - diff "~4.0.1" + diff "~5.0.0" express "~4.17.1" - glob "~7.1.4" - handlebars "~4.5.3" - http-errors "~1.7.2" - istanbul-lib-coverage "~2.0.5" - istanbul-lib-hook "~2.0.7" - istanbul-lib-instrument "~3.3.0" - istanbul-lib-report "~2.0.8" - istanbul-lib-source-maps "~3.0.6" - istanbul-reports "~2.2.6" + glob "~7.1.7" + http-errors "~1.8.0" + istanbul-lib-coverage "~3.0.0" + istanbul-lib-hook "~3.0.0" + istanbul-lib-instrument "~4.0.3" + istanbul-lib-report "~3.0.0" + istanbul-lib-source-maps "~4.0.0" + istanbul-reports "~3.0.2" lodash "~4.17.15" - mime-types "~2.1.24" + mime-types "~2.1.31" minimatch "~3.0.4" - platform "~1.3.5" - resolve "~1.11.1" - shell-quote "~1.6.1" + platform "~1.3.6" + resolve "~1.20.0" + shell-quote "~1.7.2" source-map "~0.6.1" - ts-node "^8.2.0" - tslib "~1.9.3" - ws "~7.0.0" + ts-node "~10.0.0" + tslib "~2.3.0" + ws "~7.5.2" ipaddr.js@1.9.1: version "1.9.1" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== is-core-module@^2.1.0: @@ -1504,6 +1698,13 @@ is-core-module@^2.1.0: dependencies: has "^1.0.3" +is-core-module@^2.2.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" + integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== + dependencies: + has "^1.0.3" + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -1523,8 +1724,8 @@ is-module@^1.0.0: is-natural-number@^4.0.1: version "4.0.1" - resolved "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz" - integrity sha1-q5124dtM7VHjXeDHLr7PCfc0zeg= + resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" + integrity sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ== is-number@^7.0.0: version "7.0.0" @@ -1533,8 +1734,8 @@ is-number@^7.0.0: is-stream@^1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== isarray@0.0.1: version "0.0.1" @@ -1551,61 +1752,62 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= -istanbul-lib-coverage@^2.0.5, istanbul-lib-coverage@~2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz" - integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== +istanbul-lib-coverage@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== -istanbul-lib-hook@~2.0.7: - version "2.0.7" - resolved "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz" - integrity sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA== +istanbul-lib-coverage@~3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.2.tgz#36786d4d82aad2ea5911007e255e2da6b5f80d86" + integrity sha512-o5+eTUYzCJ11/+JhW5/FUCdfsdoYVdQ/8I/OveE2XsjehYn5DdeSnNQAbjYaO8gQ6hvGTN6GM6ddQqpTVG5j8g== + +istanbul-lib-hook@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz#8f84c9434888cc6b1d0a9d7092a76d239ebf0cc6" + integrity sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ== dependencies: - append-transform "^1.0.0" + append-transform "^2.0.0" -istanbul-lib-instrument@~3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz" - integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA== - dependencies: - "@babel/generator" "^7.4.0" - "@babel/parser" "^7.4.3" - "@babel/template" "^7.4.0" - "@babel/traverse" "^7.4.3" - "@babel/types" "^7.4.0" - istanbul-lib-coverage "^2.0.5" - semver "^6.0.0" +istanbul-lib-instrument@~4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" + integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== + dependencies: + "@babel/core" "^7.7.5" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.0.0" + semver "^6.3.0" -istanbul-lib-report@~2.0.8: - version "2.0.8" - resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz" - integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ== +istanbul-lib-report@^3.0.0, istanbul-lib-report@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== dependencies: - istanbul-lib-coverage "^2.0.5" - make-dir "^2.1.0" - supports-color "^6.1.0" + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" -istanbul-lib-source-maps@~3.0.6: - version "3.0.6" - resolved "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz" - integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== +istanbul-lib-source-maps@~4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== dependencies: debug "^4.1.1" - istanbul-lib-coverage "^2.0.5" - make-dir "^2.1.0" - rimraf "^2.6.3" + istanbul-lib-coverage "^3.0.0" source-map "^0.6.1" -istanbul-reports@~2.2.6: - version "2.2.7" - resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz" - integrity sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg== +istanbul-reports@~3.0.2: + version "3.0.5" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.5.tgz#a2580107e71279ea6d661ddede929ffc6d693384" + integrity sha512-5+19PlhnGabNWB7kOFnuxT8H3T/iIyQzIbQMxXsURmmvKg86P2sbkrGOT77VnHw0Qr0gc2XzRaRfMZYYbSQCJQ== dependencies: html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" js-tokens@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^4.1.0: @@ -1617,7 +1819,7 @@ js-yaml@^4.1.0: jsesc@^2.5.1: version "2.5.2" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== json-schema-traverse@^0.4.1: @@ -1630,10 +1832,15 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= -jszip@~3.2.1: - version "3.2.2" - resolved "https://registry.npmjs.org/jszip/-/jszip-3.2.2.tgz" - integrity sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA== +json5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + +jszip@~3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.7.1.tgz#bd63401221c15625a1228c556ca8a68da6fda3d9" + integrity sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg== dependencies: lie "~3.3.0" pako "~1.0.2" @@ -1650,7 +1857,7 @@ levn@^0.4.1: lie@~3.3.0: version "3.3.0" - resolved "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== dependencies: immediate "~3.0.5" @@ -1660,11 +1867,18 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.19, lodash@^4.17.4, lodash@~4.17.15: +lodash@^4.17.4, lodash@~4.17.15: version "4.17.21" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +loupe@^2.3.1: + version "2.3.4" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.4.tgz#7e0b9bffc76f148f9be769cb1321d3dcf3cb25f3" + integrity sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ== + dependencies: + get-func-name "^2.0.0" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -1674,22 +1888,21 @@ lru-cache@^6.0.0: make-dir@^1.0.0: version "1.3.0" - resolved "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== dependencies: pify "^3.0.0" -make-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== +make-dir@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== dependencies: - pify "^4.0.1" - semver "^5.6.0" + semver "^6.0.0" make-error@^1.1.1: version "1.3.6" - resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== media-typer@0.3.0: @@ -1699,8 +1912,8 @@ media-typer@0.3.0: merge-descriptors@1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" @@ -1709,8 +1922,8 @@ merge2@^1.3.0, merge2@^1.4.1: methods@~1.1.2: version "1.1.2" - resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== micromatch@^4.0.4: version "4.0.5" @@ -1725,6 +1938,11 @@ mime-db@1.44.0: resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + mime-types@~2.1.24: version "2.1.27" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz" @@ -1732,28 +1950,37 @@ mime-types@~2.1.24: dependencies: mime-db "1.44.0" +mime-types@~2.1.31, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mime@1.6.0: version "1.6.0" - resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -minimatch@^3.0.4, minimatch@~3.0.4: +minimatch@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== dependencies: brace-expansion "^1.1.7" +minimatch@~3.0.4: + version "3.0.8" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" + integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q== + dependencies: + brace-expansion "^1.1.7" + minimist@^1.2.5: version "1.2.5" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz" - integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= - mkdirp@^0.5.1: version "0.5.5" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz" @@ -1763,19 +1990,19 @@ mkdirp@^0.5.1: ms@2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + multer@^1.4.2: version "1.4.2" resolved "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz" @@ -1795,25 +2022,25 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -negotiator@0.6.2: - version "0.6.2" - resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== -neo-async@^2.6.0: - version "2.6.2" - resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +node-releases@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.5.tgz#280ed5bc3eba0d96ce44897d8aee478bfb3d9666" + integrity sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q== object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" - resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== on-finished@^2.3.0, on-finished@~2.3.0: version "2.3.0" - resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== dependencies: ee-first "1.1.1" @@ -1824,14 +2051,6 @@ once@^1.3.0, once@^1.4.0: dependencies: wrappy "1" -optimist@^0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz" - integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - optionator@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" @@ -1846,7 +2065,7 @@ optionator@^0.9.1: pako@~1.0.2: version "1.0.11" - resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== parent-module@^1.0.0: @@ -1858,7 +2077,7 @@ parent-module@^1.0.0: parseurl@~1.3.3: version "1.3.3" - resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== path-is-absolute@^1.0.0: @@ -1878,23 +2097,28 @@ path-parse@^1.0.6: path-to-regexp@0.1.7: version "0.1.7" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pathval@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz" - integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA= +pathval@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" + integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== pend@~1.2.0: version "1.2.0" - resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz" - integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== picomatch@^2.2.2: version "2.2.2" @@ -1908,36 +2132,36 @@ picomatch@^2.3.1: pify@^2.3.0: version "2.3.0" - resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== pify@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== pinkie-promise@^2.0.0: version "2.0.1" - resolved "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw== dependencies: pinkie "^2.0.0" pinkie@^2.0.0: version "2.0.4" - resolved "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== -platform@^1.3.3, platform@~1.3.5: +platform@^1.3.3, platform@~1.3.6: version "1.3.6" - resolved "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz" + resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== +playwright-core@1.22.2: + version "1.22.2" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.22.2.tgz#ed2963d79d71c2a18d5a6fd25b60b9f0a344661a" + integrity sha512-w/hc/Ld0RM4pmsNeE6aL/fPNWw8BWit2tg+TfqJ3+p59c6s3B6C8mXvXrIPmfQEobkcFDc+4KirNzOQ+uBSP1Q== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -1960,12 +2184,12 @@ process-nextick-args@~2.0.0: resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -proxy-addr@~2.0.5: - version "2.0.6" - resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz" - integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== dependencies: - forwarded "~0.1.2" + forwarded "0.2.0" ipaddr.js "1.9.1" punycode@^2.1.0: @@ -1973,10 +2197,10 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -qs@6.7.0: - version "6.7.0" - resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz" - integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@6.9.7: + version "6.9.7" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe" + integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== queue-microtask@^1.2.2: version "1.2.3" @@ -1985,16 +2209,16 @@ queue-microtask@^1.2.2: range-parser@~1.2.1: version "1.2.1" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.4.0: - version "2.4.0" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz" - integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== +raw-body@2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.3.tgz#8f80305d11c2a0a545c2d9d89d7a0286fcead43c" + integrity sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g== dependencies: - bytes "3.1.0" - http-errors "1.7.2" + bytes "3.1.2" + http-errors "1.8.1" iconv-lite "0.4.24" unpipe "1.0.0" @@ -2010,7 +2234,7 @@ readable-stream@1.1.x: readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@~2.3.6: version "2.3.7" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== dependencies: core-util-is "~1.0.0" @@ -2039,11 +2263,12 @@ resolve@^1.17.0, resolve@^1.19.0: is-core-module "^2.1.0" path-parse "^1.0.6" -resolve@~1.11.1: - version "1.11.1" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz" - integrity sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw== +resolve@~1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== dependencies: + is-core-module "^2.2.0" path-parse "^1.0.6" reusify@^1.0.4: @@ -2051,13 +2276,6 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -2079,31 +2297,31 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@5.1.2, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.2.1, safe-buffer@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== "safer-buffer@>= 2.1.2 < 3": version "2.1.2" - resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== seek-bzip@^1.0.5: version "1.0.6" - resolved "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz" + resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.6.tgz#35c4171f55a680916b52a07859ecf3b5857f21c4" integrity sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ== dependencies: commander "^2.8.1" -semver@^5.6.0: - version "5.7.1" - resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0: +semver@^6.0.0, semver@^6.3.0: version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== semver@^7.3.5: @@ -2113,10 +2331,10 @@ semver@^7.3.5: dependencies: lru-cache "^6.0.0" -send@0.17.1: - version "0.17.1" - resolved "https://registry.npmjs.org/send/-/send-0.17.1.tgz" - integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== +send@0.17.2: + version "0.17.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820" + integrity sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww== dependencies: debug "2.6.9" depd "~1.1.2" @@ -2125,32 +2343,32 @@ send@0.17.1: escape-html "~1.0.3" etag "~1.8.1" fresh "0.5.2" - http-errors "~1.7.2" + http-errors "1.8.1" mime "1.6.0" - ms "2.1.1" + ms "2.1.3" on-finished "~2.3.0" range-parser "~1.2.1" statuses "~1.5.0" -serve-static@1.14.1: - version "1.14.1" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz" - integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== +serve-static@1.14.2: + version "1.14.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.2.tgz#722d6294b1d62626d41b43a013ece4598d292bfa" + integrity sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ== dependencies: encodeurl "~1.0.2" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.17.1" + send "0.17.2" set-immediate-shim@~1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz" - integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E= + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + integrity sha512-Li5AOqrZWCVA2n5kryzEmqai6bKSIvpz5oUJHPVj6+dsbD3X1ixtsY5tEnsaNpH3pFAHmG8eIHUrtEtohrg+UQ== -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== shebang-command@^2.0.0: version "2.0.0" @@ -2164,10 +2382,10 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@~1.6.1: - version "1.6.3" - resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.3.tgz" - integrity sha512-KvITSOPOP542Mv4lS5Cx6/qgya20Hyk+JJUdfRfikzyV6iKPszdz5TrssURXRghmi6Z9y9gATRvxJ69zD7wydQ== +shell-quote@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" + integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== slash@^3.0.0: version "3.0.0" @@ -2175,27 +2393,22 @@ slash@^3.0.0: integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== source-map-support@^0.5.17: - version "0.5.19" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" -source-map@^0.5.0: - version "0.5.7" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== streamsearch@0.1.2: version "0.1.2" @@ -2221,14 +2434,14 @@ strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== strip-dirs@^2.0.0: version "2.1.0" - resolved "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5" integrity sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g== dependencies: is-natural-number "^4.0.1" @@ -2240,18 +2453,11 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: supports-color@^5.3.0: version "5.5.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== dependencies: has-flag "^3.0.0" -supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -2261,7 +2467,7 @@ supports-color@^7.1.0: tar-stream@^1.5.2: version "1.6.2" - resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A== dependencies: bl "^1.0.0" @@ -2279,18 +2485,18 @@ text-table@^0.2.0: through@^2.3.8: version "2.3.8" - resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== to-buffer@^1.1.1: version "1.1.1" - resolved "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg== to-fast-properties@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" - integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== to-regex-range@^5.0.1: version "5.0.1" @@ -2299,17 +2505,22 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - -ts-node@^8.2.0: - version "8.10.2" - resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz" - integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== - dependencies: +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +ts-node@~10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.0.0.tgz#05f10b9a716b0b624129ad44f0ea05dac84ba3be" + integrity sha512-ROWeOIUvfFbPZkoDis0L/55Fk+6gFQNZwwKPLinacRl6tsxstTF1DbAcLKkovwnpKMVvOMHP1TIbnwXwtLg1gg== + dependencies: + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.1" arg "^4.1.0" + create-require "^1.1.0" diff "^4.0.1" make-error "^1.1.1" source-map-support "^0.5.17" @@ -2325,10 +2536,10 @@ tslib@^2.0.3: resolved "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz" integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== -tslib@~1.9.3: - version "1.9.3" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz" - integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== +tslib@~2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" + integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== tsutils@^3.21.0: version "3.21.0" @@ -2346,7 +2557,7 @@ type-check@^0.4.0, type-check@~0.4.0: type-detect@^4.0.0, type-detect@^4.0.5: version "4.0.8" - resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== type-fest@^0.20.2: @@ -2354,9 +2565,9 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18: +type-is@^1.6.4, type-is@~1.6.18: version "1.6.18" - resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== dependencies: media-typer "0.3.0" @@ -2372,14 +2583,9 @@ typescript@^4.6.3: resolved "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz" integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw== -uglify-js@^3.1.4: - version "3.12.1" - resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.12.1.tgz" - integrity sha512-o8lHP20KjIiQe5b/67Rh68xEGRrc2SRsCuuoYclXXoC74AfSRGblU1HKzJWH3HxPZ+Ort85fWHpSX7KwBUC9CQ== - unbzip2-stream@^1.0.9: version "1.4.3" - resolved "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== dependencies: buffer "^5.2.1" @@ -2387,8 +2593,8 @@ unbzip2-stream@^1.0.9: unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== uri-js@^4.2.2: version "4.4.1" @@ -2404,8 +2610,8 @@ util-deprecate@~1.0.1: utils-merge@1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" - integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== v8-compile-cache@^2.0.3: version "2.3.0" @@ -2414,8 +2620,8 @@ v8-compile-cache@^2.0.3: vary@~1.1.2: version "1.1.2" - resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== which@^2.0.1: version "2.0.2" @@ -2429,22 +2635,15 @@ word-wrap@^1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" - integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= - wrappy@1: version "1.0.2" resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -ws@~7.0.0: - version "7.0.1" - resolved "https://registry.npmjs.org/ws/-/ws-7.0.1.tgz" - integrity sha512-ILHfMbuqLJvnSgYXLgy4kMntroJpe8hT41dOVWM8bxRuw6TK4mgMp9VJUNsZTEc5Bh+Mbs0DJT4M0N+wBG9l9A== - dependencies: - async-limiter "^1.0.0" +ws@~7.5.2: + version "7.5.8" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.8.tgz#ac2729881ab9e7cbaf8787fe3469a48c5c7f636a" + integrity sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw== xtend@^4.0.0: version "4.0.2" @@ -2458,13 +2657,13 @@ yallist@^4.0.0: yauzl@^2.4.2: version "2.10.0" - resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz" - integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== dependencies: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" yn@3.1.1: version "3.1.1" - resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== From d7430853cf3b46c33b4e1fe4b850266c19aea41a Mon Sep 17 00:00:00 2001 From: Manuel Puyol Date: Fri, 15 Jul 2022 18:30:47 -0500 Subject: [PATCH 02/13] Add original click event to 'turbo:click' details (#611) Co-authored-by: David Heinemeier Hansson --- src/core/session.ts | 12 ++++++------ src/observers/link_click_observer.ts | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/core/session.ts b/src/core/session.ts index a54231640..727019ba5 100644 --- a/src/core/session.ts +++ b/src/core/session.ts @@ -150,11 +150,11 @@ export class Session // Link click observer delegate - willFollowLinkToLocation(link: Element, location: URL) { + willFollowLinkToLocation(link: Element, location: URL, event: MouseEvent) { return ( this.elementDriveEnabled(link) && locationIsVisitable(location, this.snapshot.rootLocation) && - this.applicationAllowsFollowingLinkToLocation(link, location) + this.applicationAllowsFollowingLinkToLocation(link, location, event) ) } @@ -300,8 +300,8 @@ export class Session // Application events - applicationAllowsFollowingLinkToLocation(link: Element, location: URL) { - const event = this.notifyApplicationAfterClickingLinkToLocation(link, location) + applicationAllowsFollowingLinkToLocation(link: Element, location: URL, ev: MouseEvent) { + const event = this.notifyApplicationAfterClickingLinkToLocation(link, location, ev) return !event.defaultPrevented } @@ -310,10 +310,10 @@ export class Session return !event.defaultPrevented } - notifyApplicationAfterClickingLinkToLocation(link: Element, location: URL) { + notifyApplicationAfterClickingLinkToLocation(link: Element, location: URL, event: MouseEvent) { return dispatch("turbo:click", { target: link, - detail: { url: location.href }, + detail: { url: location.href, originalEvent: event }, cancelable: true, }) } diff --git a/src/observers/link_click_observer.ts b/src/observers/link_click_observer.ts index f0b0282ed..442f2495e 100644 --- a/src/observers/link_click_observer.ts +++ b/src/observers/link_click_observer.ts @@ -1,7 +1,7 @@ import { expandURL } from "../core/url" export interface LinkClickObserverDelegate { - willFollowLinkToLocation(link: Element, location: URL): boolean + willFollowLinkToLocation(link: Element, location: URL, event: MouseEvent): boolean followedLinkToLocation(link: Element, location: URL): void } @@ -38,7 +38,7 @@ export class LinkClickObserver { const link = this.findLinkFromClickTarget(target) if (link) { const location = this.getLocationForLink(link) - if (this.delegate.willFollowLinkToLocation(link, location)) { + if (this.delegate.willFollowLinkToLocation(link, location, event)) { event.preventDefault() this.delegate.followedLinkToLocation(link, location) } From 706e614221b21e3854fd92e4336a67338e3c485e Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 15 Jul 2022 16:48:44 -0700 Subject: [PATCH 03/13] Add .php as a valid isHTML extension (#629) Before we fully resolve #519, we can sort out the main hurt from this, which seems to be .php extensions. --- src/core/url.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/url.ts b/src/core/url.ts index 2aa343276..0e45d8f2b 100644 --- a/src/core/url.ts +++ b/src/core/url.ts @@ -25,7 +25,7 @@ export function getExtension(url: URL) { } export function isHTML(url: URL) { - return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml))$/) + return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml|php))$/) } export function isPrefixedBy(baseURL: URL, url: URL) { From 2d5cdda4c030658da21965cb20d2885ca7c3e127 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Fri, 15 Jul 2022 19:53:46 -0400 Subject: [PATCH 04/13] Export Type declarations for `turbo:` events (#452) Various `turbo:`-prefixed events are dispatched as [CustomEvent][] instances with data encoded into the [detail][] property. In TypeScript, that property is encoded as `any`, but the `CustomEvent` type is generic (i.e. `CustomEvent`) where the generic Type argument describes the structure of the `detail` key. This commit introduces types that extend from `CustomEvent` for each event, and exports them from `/core/index.ts`, which is exported from `/index.ts` in-turn. In practice, there are no changes to the implementation. However, TypeScript consumers of the package can import the types. At the same time, the internal implementation can depend on the types to ensure consistency throughout. [CustomEvent]: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent [detail]: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail --- src/core/drive/form_submission.ts | 9 +++++++-- src/core/frames/link_interceptor.ts | 8 +++++--- src/core/index.ts | 15 +++++++++++++++ src/core/session.ts | 27 ++++++++++++++++++--------- src/elements/stream_element.ts | 4 +++- src/http/fetch_request.ts | 13 +++++++++++-- src/observers/cache_observer.ts | 6 ++++-- src/observers/stream_observer.ts | 5 +++-- src/util.ts | 11 +++++++---- 9 files changed, 73 insertions(+), 25 deletions(-) diff --git a/src/core/drive/form_submission.ts b/src/core/drive/form_submission.ts index 56a1fc525..244461352 100644 --- a/src/core/drive/form_submission.ts +++ b/src/core/drive/form_submission.ts @@ -29,6 +29,11 @@ enum FormEnctype { plain = "text/plain", } +export type TurboSubmitStartEvent = CustomEvent<{ formSubmission: FormSubmission }> +export type TurboSubmitEndEvent = CustomEvent< + { formSubmission: FormSubmission } & { [K in keyof FormSubmissionResult]?: FormSubmissionResult[K] } +> + function formEnctypeFromString(encoding: string): FormEnctype { switch (encoding.toLowerCase()) { case FormEnctype.multipart: @@ -163,7 +168,7 @@ export class FormSubmission { requestStarted(_request: FetchRequest) { this.state = FormSubmissionState.waiting this.submitter?.setAttribute("disabled", "") - dispatch("turbo:submit-start", { + dispatch("turbo:submit-start", { target: this.formElement, detail: { formSubmission: this }, }) @@ -200,7 +205,7 @@ export class FormSubmission { requestFinished(_request: FetchRequest) { this.state = FormSubmissionState.stopped this.submitter?.removeAttribute("disabled") - dispatch("turbo:submit-end", { + dispatch("turbo:submit-end", { target: this.formElement, detail: { formSubmission: this, ...this.result }, }) diff --git a/src/core/frames/link_interceptor.ts b/src/core/frames/link_interceptor.ts index 53ff4b31b..65ff1066f 100644 --- a/src/core/frames/link_interceptor.ts +++ b/src/core/frames/link_interceptor.ts @@ -1,3 +1,5 @@ +import { TurboClickEvent, TurboBeforeVisitEvent } from "../session" + export interface LinkInterceptorDelegate { shouldInterceptLinkClick(element: Element, url: string): boolean linkClickIntercepted(element: Element, url: string): void @@ -33,7 +35,7 @@ export class LinkInterceptor { } } - linkClicked = ((event: CustomEvent) => { + linkClicked = ((event: TurboClickEvent) => { if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) { if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url)) { this.clickEvent.preventDefault() @@ -44,9 +46,9 @@ export class LinkInterceptor { delete this.clickEvent }) - willVisit = () => { + willVisit = ((_event: TurboBeforeVisitEvent) => { delete this.clickEvent - } + }) respondsToEventTarget(target: EventTarget | null) { const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null diff --git a/src/core/index.ts b/src/core/index.ts index f736483cf..24136d790 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -12,6 +12,21 @@ import { FormSubmission } from "./drive/form_submission" const session = new Session() const { navigator } = session export { navigator, session, PageRenderer, PageSnapshot, FrameRenderer } +export { + TurboBeforeCacheEvent, + TurboBeforeRenderEvent, + TurboBeforeVisitEvent, + TurboClickEvent, + TurboFrameLoadEvent, + TurboFrameRenderEvent, + TurboLoadEvent, + TurboRenderEvent, + TurboVisitEvent, +} from "./session" + +export { TurboSubmitStartEvent, TurboSubmitEndEvent } from "./drive/form_submission" +export { TurboBeforeFetchRequestEvent, TurboBeforeFetchResponseEvent } from "../http/fetch_request" +export { TurboBeforeStreamRenderEvent } from "../elements/stream_element" /** * Starts the main session. diff --git a/src/core/session.ts b/src/core/session.ts index 727019ba5..4b723577f 100644 --- a/src/core/session.ts +++ b/src/core/session.ts @@ -21,6 +21,15 @@ import { FetchResponse } from "../http/fetch_response" import { Preloader, PreloaderDelegate } from "./drive/preloader" export type TimingData = unknown +export type TurboBeforeCacheEvent = CustomEvent +export type TurboBeforeRenderEvent = CustomEvent<{ newBody: HTMLBodyElement; resume: (value: any) => void }> +export type TurboBeforeVisitEvent = CustomEvent<{ url: string }> +export type TurboClickEvent = CustomEvent<{ url: string; originalEvent: MouseEvent }> +export type TurboFrameLoadEvent = CustomEvent +export type TurboFrameRenderEvent = CustomEvent<{ fetchResponse: FetchResponse }> +export type TurboLoadEvent = CustomEvent<{ url: string; timing: TimingData }> +export type TurboRenderEvent = CustomEvent +export type TurboVisitEvent = CustomEvent<{ url: string; action: Action }> export class Session implements @@ -311,7 +320,7 @@ export class Session } notifyApplicationAfterClickingLinkToLocation(link: Element, location: URL, event: MouseEvent) { - return dispatch("turbo:click", { + return dispatch("turbo:click", { target: link, detail: { url: location.href, originalEvent: event }, cancelable: true, @@ -319,7 +328,7 @@ export class Session } notifyApplicationBeforeVisitingLocation(location: URL) { - return dispatch("turbo:before-visit", { + return dispatch("turbo:before-visit", { detail: { url: location.href }, cancelable: true, }) @@ -327,27 +336,27 @@ export class Session notifyApplicationAfterVisitingLocation(location: URL, action: Action) { markAsBusy(document.documentElement) - return dispatch("turbo:visit", { detail: { url: location.href, action } }) + return dispatch("turbo:visit", { detail: { url: location.href, action } }) } notifyApplicationBeforeCachingSnapshot() { - return dispatch("turbo:before-cache") + return dispatch("turbo:before-cache") } notifyApplicationBeforeRender(newBody: HTMLBodyElement, resume: (value: any) => void) { - return dispatch("turbo:before-render", { + return dispatch("turbo:before-render", { detail: { newBody, resume }, cancelable: true, }) } notifyApplicationAfterRender() { - return dispatch("turbo:render") + return dispatch("turbo:render") } notifyApplicationAfterPageLoad(timing: TimingData = {}) { clearBusyState(document.documentElement) - return dispatch("turbo:load", { + return dispatch("turbo:load", { detail: { url: this.location.href, timing }, }) } @@ -362,11 +371,11 @@ export class Session } notifyApplicationAfterFrameLoad(frame: FrameElement) { - return dispatch("turbo:frame-load", { target: frame }) + return dispatch("turbo:frame-load", { target: frame }) } notifyApplicationAfterFrameRender(fetchResponse: FetchResponse, frame: FrameElement) { - return dispatch("turbo:frame-render", { + return dispatch("turbo:frame-render", { detail: { fetchResponse }, target: frame, cancelable: true, diff --git a/src/elements/stream_element.ts b/src/elements/stream_element.ts index 8d3475ccf..4e803a3cf 100644 --- a/src/elements/stream_element.ts +++ b/src/elements/stream_element.ts @@ -1,6 +1,8 @@ import { StreamActions } from "../core/streams/stream_actions" import { nextAnimationFrame } from "../util" +export type TurboBeforeStreamRenderEvent = CustomEvent + //