diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efbfaa079..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,22 +19,33 @@ 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 run: | CHROMEVER="$(chromedriver --version | cut -d' ' -f2)" echo "Actions ChromeDriver is $CHROMEVER" - CONTENTS="$(jq '.tunnelOptions.drivers[0].name = "chrome"' < intern.json)" - CONTENTS="$(echo ${CONTENTS} | jq --arg chromever "$CHROMEVER" '.tunnelOptions.drivers[0].version = $chromever')" - echo "${CONTENTS}" > intern.json - cat intern.json + echo "CHROMEVER=${CHROMEVER}" >> $GITHUB_ENV - 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 81e372f98..5106f6a85 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,16 @@ 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: @@ -42,10 +51,29 @@ The tests are using the compiled version of the library and they are themselves 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:browser --project=chrome +``` + +To run the browser tests in a "headed" browser, pass the `--headed` flag: ```bash -yarn test +yarn test:browser --project=chrome --headed ``` ### Test files @@ -55,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/core/drive/form_submission.ts b/src/core/drive/form_submission.ts index 94dbd7c5f..244461352 100644 --- a/src/core/drive/form_submission.ts +++ b/src/core/drive/form_submission.ts @@ -1,7 +1,7 @@ import { FetchRequest, FetchMethod, fetchMethodFromString, FetchRequestHeaders } from "../../http/fetch_request" import { FetchResponse } from "../../http/fetch_response" import { expandURL } from "../url" -import { dispatch } from "../../util" +import { attributeTrue, dispatch } from "../../util" import { StreamMessage } from "../streams/stream_message" export interface FormSubmissionDelegate { @@ -158,6 +158,9 @@ export class FormSubmission { if (token) { headers["X-CSRF-Token"] = token } + } + + if (this.requestAcceptsTurboStreamResponse(request)) { headers["Accept"] = [StreamMessage.contentType, headers["Accept"]].join(", ") } } @@ -209,9 +212,15 @@ export class FormSubmission { this.delegate.formSubmissionFinished(this) } + // Private + requestMustRedirect(request: FetchRequest) { return !request.isIdempotent && this.mustRedirect } + + requestAcceptsTurboStreamResponse(request: FetchRequest) { + return !request.isIdempotent || attributeTrue(this.formElement, "data-turbo-stream") + } } function buildFormData(formElement: HTMLFormElement, submitter?: HTMLElement): FormData { diff --git a/src/core/drive/navigator.ts b/src/core/drive/navigator.ts index cd8603f17..935f9eea7 100644 --- a/src/core/drive/navigator.ts +++ b/src/core/drive/navigator.ts @@ -108,9 +108,9 @@ export class Navigator { if (responseHTML) { const snapshot = PageSnapshot.fromHTMLString(responseHTML) if (fetchResponse.serverError) { - await this.view.renderError(snapshot) + await this.view.renderError(snapshot, this.currentVisit) } else { - await this.view.renderPage(snapshot) + await this.view.renderPage(snapshot, false, true, this.currentVisit) } this.view.scrollToTop() this.view.clearSnapshotCache() diff --git a/src/core/drive/page_view.ts b/src/core/drive/page_view.ts index 19c54dd67..042ea748e 100644 --- a/src/core/drive/page_view.ts +++ b/src/core/drive/page_view.ts @@ -4,6 +4,7 @@ import { ErrorRenderer } from "./error_renderer" import { PageRenderer } from "./page_renderer" import { PageSnapshot } from "./page_snapshot" import { SnapshotCache } from "./snapshot_cache" +import { Visit } from "./visit" export interface PageViewDelegate extends ViewDelegate { viewWillCacheSnapshot(): void @@ -16,15 +17,18 @@ export class PageView extends View {} private currentFetchRequest: FetchRequest | null = null private resolveVisitPromise = () => {} private connected = false private hasBeenLoaded = false - private settingSourceURL = false + private ignoredAttributes: Set = new Set() constructor(element: FrameElement) { this.element = element @@ -49,13 +53,13 @@ export class FrameController connect() { if (!this.connected) { this.connected = true - this.reloadable = false if (this.loadingStyle == FrameLoadingStyle.lazy) { this.appearanceObserver.start() + } else { + this.loadSourceURL() } this.linkInterceptor.start() this.formInterceptor.start() - this.sourceURLChanged() } } @@ -75,11 +79,23 @@ export class FrameController } sourceURLChanged() { + if (this.isIgnoringChangesTo("src")) return + + if (this.element.isConnected) { + this.complete = false + } + if (this.loadingStyle == FrameLoadingStyle.eager || this.hasBeenLoaded) { this.loadSourceURL() } } + completeChanged() { + if (this.isIgnoringChangesTo("complete")) return + + this.loadSourceURL() + } + loadingStyleChanged() { if (this.loadingStyle == FrameLoadingStyle.lazy) { this.appearanceObserver.start() @@ -89,26 +105,12 @@ export class FrameController } } - async loadSourceURL() { - if ( - !this.settingSourceURL && - this.enabled && - this.isActive && - (this.reloadable || this.sourceURL != this.currentURL) - ) { - const previousURL = this.currentURL - this.currentURL = this.sourceURL - if (this.sourceURL) { - try { - this.element.loaded = this.visit(expandURL(this.sourceURL)) - this.appearanceObserver.stop() - await this.element.loaded - this.hasBeenLoaded = true - } catch (error) { - this.currentURL = previousURL - throw error - } - } + private async loadSourceURL() { + if (this.enabled && this.isActive && !this.complete && this.sourceURL) { + this.element.loaded = this.visit(expandURL(this.sourceURL)) + this.appearanceObserver.stop() + await this.element.loaded + this.hasBeenLoaded = true } } @@ -125,6 +127,7 @@ export class FrameController const renderer = new FrameRenderer(this.view.snapshot, snapshot, false, false) if (this.view.renderPromise) await this.view.renderPromise await this.view.render(renderer) + this.complete = true session.frameRendered(fetchResponse, this.element) session.frameLoaded(this.element) this.fetchResponseLoaded(fetchResponse) @@ -146,7 +149,7 @@ export class FrameController // Link interceptor delegate shouldInterceptLinkClick(element: Element, _url: string) { - if (element.hasAttribute("data-turbo-method")) { + if (element.hasAttribute("data-turbo-method") || attributeTrue(element, "data-turbo-stream")) { return false } else { return this.shouldInterceptNavigation(element) @@ -154,7 +157,6 @@ export class FrameController } linkClickIntercepted(element: Element, url: string) { - this.reloadable = true this.navigateFrame(element, url) } @@ -169,7 +171,6 @@ export class FrameController this.formSubmission.stop() } - this.reloadable = false this.formSubmission = new FormSubmission(this, element, submitter) const { fetchRequest } = this.formSubmission this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest) @@ -272,7 +273,6 @@ export class FrameController this.proposeVisitIfNavigatedWithAction(frame, element, submitter) - frame.setAttribute("reloadable", "") frame.src = url } @@ -308,12 +308,12 @@ export class FrameController const id = CSS.escape(this.id) try { - element = activateElement(container.querySelector(`turbo-frame#${id}`), this.currentURL) + element = activateElement(container.querySelector(`turbo-frame#${id}`), this.sourceURL) if (element) { return element } - element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.currentURL) + element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.sourceURL) if (element) { await element.loaded return await this.extractForeignFrameElement(element) @@ -379,24 +379,9 @@ export class FrameController } set sourceURL(sourceURL: string | undefined) { - this.settingSourceURL = true - this.element.src = sourceURL ?? null - this.currentURL = this.element.src - this.settingSourceURL = false - } - - get reloadable() { - const frame = this.findFrameElement(this.element) - return frame.hasAttribute("reloadable") - } - - set reloadable(value: boolean) { - const frame = this.findFrameElement(this.element) - if (value) { - frame.setAttribute("reloadable", "") - } else { - frame.removeAttribute("reloadable") - } + this.ignoringChangesToAttribute("src", () => { + this.element.src = sourceURL ?? null + }) } get loadingStyle() { @@ -407,6 +392,20 @@ export class FrameController return this.formSubmission !== undefined || this.resolveVisitPromise() !== undefined } + get complete() { + return this.element.hasAttribute("complete") + } + + set complete(value: boolean) { + this.ignoringChangesToAttribute("complete", () => { + if (value) { + this.element.setAttribute("complete", "") + } else { + this.element.removeAttribute("complete") + } + }) + } + get isActive() { return this.element.isActive && this.connected } @@ -416,6 +415,16 @@ export class FrameController const root = meta?.content ?? "/" return expandURL(root) } + + private isIgnoringChangesTo(attributeName: FrameElementObservedAttribute): boolean { + return this.ignoredAttributes.has(attributeName) + } + + private ignoringChangesToAttribute(attributeName: FrameElementObservedAttribute, callback: () => void) { + this.ignoredAttributes.add(attributeName) + callback() + this.ignoredAttributes.delete(attributeName) + } } class SnapshotSubstitution { diff --git a/src/core/frames/frame_redirector.ts b/src/core/frames/frame_redirector.ts index 8e41aea8e..1ddb5a0d5 100644 --- a/src/core/frames/frame_redirector.ts +++ b/src/core/frames/frame_redirector.ts @@ -42,7 +42,6 @@ export class FrameRedirector implements LinkInterceptorDelegate, FormInterceptor formSubmissionIntercepted(element: HTMLFormElement, submitter?: HTMLElement) { const frame = this.findFrameElement(element, submitter) if (frame) { - frame.removeAttribute("reloadable") frame.delegate.formSubmissionIntercepted(element, submitter) } } diff --git a/src/core/native/browser_adapter.ts b/src/core/native/browser_adapter.ts index 7fc70317f..20792025e 100644 --- a/src/core/native/browser_adapter.ts +++ b/src/core/native/browser_adapter.ts @@ -17,6 +17,7 @@ export class BrowserAdapter implements Adapter { visitProgressBarTimeout?: number formProgressBarTimeout?: number + location?: URL constructor(session: Session) { this.session = session @@ -27,9 +28,9 @@ export class BrowserAdapter implements Adapter { } visitStarted(visit: Visit) { + this.location = visit.location visit.loadCachedSnapshot() visit.issueRequest() - visit.changeHistory() visit.goToSamePageAnchor() } @@ -121,7 +122,10 @@ export class BrowserAdapter implements Adapter { reload(reason: ReloadReason) { dispatch("turbo:reload", { detail: reason }) - window.location.reload() + + if (!this.location) return + + window.location.href = this.location.toString() } get navigator() { diff --git a/src/core/session.ts b/src/core/session.ts index abefe64d2..4de2131ae 100644 --- a/src/core/session.ts +++ b/src/core/session.ts @@ -12,7 +12,7 @@ import { ScrollObserver } from "../observers/scroll_observer" import { StreamMessage } from "./streams/stream_message" import { StreamObserver } from "../observers/stream_observer" import { Action, Position, StreamSource, isAction } from "./types" -import { clearBusyState, dispatch, markAsBusy } from "../util" +import { attributeTrue, clearBusyState, dispatch, markAsBusy } from "../util" import { PageView, PageViewDelegate } from "./drive/page_view" import { Visit, VisitOptions } from "./drive/visit" import { PageSnapshot } from "./drive/page_snapshot" @@ -174,16 +174,20 @@ export class Session convertLinkWithMethodClickToFormSubmission(link: Element) { const linkMethod = link.getAttribute("data-turbo-method") + const useTurboStream = attributeTrue(link, "data-turbo-stream") - if (linkMethod) { + if (linkMethod || useTurboStream) { const form = document.createElement("form") - form.setAttribute("method", linkMethod) + form.setAttribute("method", linkMethod || "get") form.action = link.getAttribute("href") || "undefined" form.hidden = true - if (link.hasAttribute("data-turbo-confirm")) { - form.setAttribute("data-turbo-confirm", link.getAttribute("data-turbo-confirm")!) - } + const attributes = ["data-turbo-confirm", "data-turbo-stream"] + attributes.forEach((attribute) => { + if (link.hasAttribute(attribute)) { + form.setAttribute(attribute, link.getAttribute(attribute)!) + } + }) const frame = this.getTargetFrameForLink(link) if (frame) { diff --git a/src/elements/frame_element.ts b/src/elements/frame_element.ts index d64e552e0..3f40937d7 100644 --- a/src/elements/frame_element.ts +++ b/src/elements/frame_element.ts @@ -5,9 +5,12 @@ export enum FrameLoadingStyle { lazy = "lazy", } +export type FrameElementObservedAttribute = keyof FrameElement & ("disabled" | "complete" | "loading" | "src") + export interface FrameElementDelegate { connect(): void disconnect(): void + completeChanged(): void loadingStyleChanged(): void sourceURLChanged(): void disabledChanged(): void @@ -40,8 +43,8 @@ export class FrameElement extends HTMLElement { loaded: Promise = Promise.resolve() readonly delegate: FrameElementDelegate - static get observedAttributes() { - return ["disabled", "loading", "src"] + static get observedAttributes(): FrameElementObservedAttribute[] { + return ["disabled", "complete", "loading", "src"] } constructor() { @@ -59,6 +62,7 @@ export class FrameElement extends HTMLElement { reload() { const { src } = this + this.removeAttribute("complete") this.src = null this.src = src } @@ -66,6 +70,8 @@ export class FrameElement extends HTMLElement { attributeChangedCallback(name: string) { if (name == "loading") { this.delegate.loadingStyleChanged() + } else if (name == "complete") { + this.delegate.completeChanged() } else if (name == "src") { this.delegate.sourceURLChanged() } else { diff --git a/src/elements/index.ts b/src/elements/index.ts index b5683ac25..730a3a7b5 100644 --- a/src/elements/index.ts +++ b/src/elements/index.ts @@ -1,6 +1,7 @@ import { FrameController } from "../core/frames/frame_controller" import { FrameElement } from "./frame_element" import { StreamElement } from "./stream_element" +import { StreamSourceElement } from "./stream_source_element" FrameElement.delegateConstructor = FrameController @@ -14,3 +15,7 @@ if (customElements.get("turbo-frame") === undefined) { if (customElements.get("turbo-stream") === undefined) { customElements.define("turbo-stream", StreamElement) } + +if (customElements.get("turbo-stream-source") === undefined) { + customElements.define("turbo-stream-source", StreamSourceElement) +} diff --git a/src/elements/stream_source_element.ts b/src/elements/stream_source_element.ts new file mode 100644 index 000000000..d2439dc41 --- /dev/null +++ b/src/elements/stream_source_element.ts @@ -0,0 +1,22 @@ +import { StreamSource } from "../core/types" +import { connectStreamSource, disconnectStreamSource } from "../index" + +export class StreamSourceElement extends HTMLElement { + streamSource: StreamSource | null = null + + connectedCallback() { + this.streamSource = this.src.match(/^ws{1,2}:/) ? new WebSocket(this.src) : new EventSource(this.src) + + connectStreamSource(this.streamSource) + } + + disconnectedCallback() { + if (this.streamSource) { + disconnectStreamSource(this.streamSource) + } + } + + get src(): string { + return this.getAttribute("src") || "" + } +} diff --git a/src/tests/fixtures/form.html b/src/tests/fixtures/form.html index 19ec666ef..cbdbb0bc9 100644 --- a/src/tests/fixtures/form.html +++ b/src/tests/fixtures/form.html @@ -25,6 +25,11 @@

Form

+
+ + + +

@@ -254,6 +259,8 @@

Frame: Form

Break-out of frame with method link inside frame
Method link inside frame targeting another frame
Stream link inside frame + Stream link GET inside frame + Stream link (no method) inside frame Stream link inside frame with confirmation Method link within form inside frame
@@ -283,6 +290,7 @@

Frame: Form

Method link outside frame
Stream link outside frame + Stream link (no method) outside frame Method link within form outside frame
Stream link within form outside frame diff --git a/src/tests/fixtures/loading.html b/src/tests/fixtures/loading.html index 9c0b9a673..8b925933d 100644 --- a/src/tests/fixtures/loading.html +++ b/src/tests/fixtures/loading.html @@ -1,5 +1,5 @@ - + Turbo @@ -13,6 +13,8 @@ + Navigate #loading-lazy turbo-frame +
Eager-loaded diff --git a/src/tests/fixtures/rendering.html b/src/tests/fixtures/rendering.html index 88b3f6099..a10308f8e 100644 --- a/src/tests/fixtures/rendering.html +++ b/src/tests/fixtures/rendering.html @@ -78,6 +78,7 @@

Rendering


+

Visit control: reload - middle

Visit control: reload

diff --git a/src/tests/fixtures/stream.html b/src/tests/fixtures/stream.html index 36d18870a..9b591a7b5 100644 --- a/src/tests/fixtures/stream.html +++ b/src/tests/fixtures/stream.html @@ -4,8 +4,9 @@ Turbo Streams - + + @@ -19,6 +20,11 @@ +
+ + +
+
First
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/fixtures/visit_control_reload.html b/src/tests/fixtures/visit_control_reload.html index 1b0ecaad8..9707700d4 100644 --- a/src/tests/fixtures/visit_control_reload.html +++ b/src/tests/fixtures/visit_control_reload.html @@ -6,8 +6,17 @@ - - -

Visit control: reload

+ + + +

Visit control: reload

+ +
+ +

Middle the page

+
+

Down the page

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 e407d9055..cc713df85 100644 --- a/src/tests/functional/form_submission_tests.ts +++ b/src/tests/functional/form_submission_tests.ts @@ -1,933 +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]") - - 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") - - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") - - this.assert.ok(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 POST form submission merges values from both searchParams and body"() { - await this.clickSelector("#form-action-post-redirect-self-q-b") - await this.nextBody - - 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 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") - } - - async "test standard GET form submission does not merge values into the [action] attribute"() { - 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-self-q-b") - await this.nextBody +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.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 - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.getSearchParam("query"), "2") - } - - 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 - - const htmlAfter = await this.outerHTMLForSelector("body") - this.assert.equal(htmlAfter, htmlBefore) - } - - 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 + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(getSearchParam(page.url(), "query"), "2") +}) - const htmlAfter = await this.outerHTMLForSelector("body") - this.assert.equal(htmlAfter, htmlBefore) - } +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() - 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 + const htmlAfter = await outerHTMLForSelector(page, "body") + assert.equal(htmlAfter, htmlBefore) +}) - 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 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) - 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 + 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") +}) - 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 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) - async "test submitter form submission reads button attributes"() { - const button = await this.querySelector("#submitter form button[type=submit]") - await button.click() - await this.nextBody + 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") +}) - this.assert.equal(await this.pathname, "/src/tests/fixtures/two.html") - this.assert.equal(await this.visitAction, "advance") - } +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) - 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.equal(pathname(page.url()), "/src/tests/fixtures/two.html") + assert.equal(await visitAction(page), "advance") +}) - const enctype = await this.getSearchParam("enctype") - this.assert.ok(enctype?.startsWith("multipart/form-data"), "submits a multipart/form-data request") - } +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() - 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 + const enctype = getSearchParam(page.url(), "enctype") + assert.ok(enctype?.startsWith("multipart/form-data"), "submits a multipart/form-data request") +}) - 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 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() - 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 + 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") +}) - 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") - } - - 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") +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() - this.assert.notOk(await this.formSubmitStarted) - } + 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 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 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 form submission skipped within method=dialog"() { - await this.clickSelector('#dialog-method [type="submit"]') - await this.nextBeat + assert.equal(await page.textContent("#frame h2"), "Frame: Form") + assert.equal(await page.getAttribute("#frame", "src"), null, "does not navigate frame") +}) - this.assert.notOk(await this.formSubmitStarted) - } +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 form submission skipped with submitter formmethod=dialog"() { - await this.clickSelector('#dialog-formmethod-turbo-frame [formmethod="dialog"]') - await this.nextBeat + assert.notOk(await formSubmitStarted(page)) +}) - this.assert.notOk(await this.formSubmitEnded) - } +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 form submission targetting frame skipped within method=dialog"() { - await this.clickSelector("#dialog-method-turbo-frame button") - await this.nextBeat + assert.notOk(await formSubmitStarted(page)) +}) - this.assert.notOk(await this.formSubmitEnded) - } +test("test form submission skipped within method=dialog", async ({ page }) => { + await page.click('#dialog-method [type="submit"]') + await nextBeat() - async "test form submission targetting frame skipped with submitter formmethod=dialog"() { - await this.clickSelector('#dialog-formmethod [formmethod="dialog"]') - await this.nextBeat + assert.notOk(await formSubmitStarted(page)) +}) - this.assert.notOk(await this.formSubmitStarted) - } +test("test form submission skipped with submitter formmethod=dialog", async ({ page }) => { + await page.click('#dialog-formmethod-turbo-frame [formmethod="dialog"]') + await nextBeat() - 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 + assert.notOk(await formSubmitEnded(page)) +}) - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - } +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 targeting a frame submits the Turbo-Frame header"() { - await this.clickSelector('#targets-frame [type="submit"]') + assert.notOk(await formSubmitEnded(page)) +}) - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") +test("test form submission targetting frame skipped with submitter formmethod=dialog", async ({ page }) => { + await page.click('#dialog-formmethod [formmethod="dialog"]') + await nextBeat() - this.assert.ok(fetchOptions.headers["Turbo-Frame"], "submits with the Turbo-Frame header") - } + assert.notOk(await formSubmitStarted(page)) +}) - async "test link method form submission inside frame"() { - await this.clickSelector("#link-method-inside-frame") - await this.nextBeat +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) - const title = await this.querySelector("#frame h2") - this.assert.equal(await title.getVisibleText(), "Frame: Loaded") - this.assert.notOk(await this.hasSelector("#nested-child")) - } + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.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 form submission targeting a frame submits the Turbo-Frame header", async ({ page }) => { + await page.click('#targets-frame [type="submit"]') - 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["Turbo-Frame"], "submits with the Turbo-Frame header") +}) - 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 link method form submission inside frame", async ({ page }) => { + await page.click("#link-method-inside-frame") + await nextBeat() - async "test stream link method form submission inside frame"() { - await this.clickSelector("#stream-link-method-inside-frame") - await this.nextBeat + assert.equal(await await page.textContent("#frame h2"), "Frame: Loaded") + assert.notOk(await hasSelector(page, "#nested-child")) +}) - const message = await this.querySelector("#frame div.message") - this.assert.equal(await message.getVisibleText(), "Link!") - } +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) - 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 link method form submission inside frame with data-turbo-frame target", async ({ page }) => { + await page.click("#link-method-inside-frame-with-target") + await nextBeat() - async "test link method form submission inside frame with confirmation confirmed"() { - await this.clickSelector("#link-method-inside-frame-with-confirmation") + 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") +}) - this.assert.equal(await this.getAlertText(), "Are you sure?") - await this.acceptAlert() +test("test stream link method form submission inside frame", async ({ page }) => { + await page.click("#stream-link-method-inside-frame") + await nextBeat() - await this.nextBeat + const message = page.locator("#frame div.message") + assert.equal(await message.textContent(), "Link!") +}) - const message = await this.querySelector("#frame div.message") - this.assert.equal(await message.getVisibleText(), "Link!") - } +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 with confirmation cancelled"() { - await this.clickSelector("#link-method-inside-frame-with-confirmation") + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") - this.assert.equal(await this.getAlertText(), "Are you sure?") - await this.dismissAlert() + assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) +}) - await this.nextBeat +test("test stream link inside frame", async ({ page }) => { + await page.click("#stream-link-inside-frame") - this.assert.notOk( - await this.hasSelector("#frame div.message"), - "Not confirming form submission does not submit the form" - ) - } + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") - async "test link method form submission outside frame"() { - await this.clickSelector("#link-method-outside-frame") - await this.nextBody + assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) +}) - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "Hello") - } +test("test stream link outside frame", async ({ page }) => { + await page.click("#stream-link-outside-frame") - async "test stream link method form submission outside frame"() { - await this.clickSelector("#stream-link-method-outside-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 link method form submission within form outside frame"() { - await this.clickSelector("#link-method-within-form-outside-frame") - await this.nextBody +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 title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "Hello") - } + const message = page.locator("#frame div.message") + assert.equal(await message.textContent(), "Link!") +}) - async "test stream link method form submission within form outside frame"() { - await this.clickSelector("#stream-link-method-within-form-outside-frame") - await this.nextBeat +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() + }) - const message = await this.querySelector("#frame div.message") - this.assert.equal(await message.getVisibleText(), "Link!") - } + await page.click("#link-method-inside-frame-with-confirmation") + await nextBeat() - 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]") + const message = page.locator("#frame div.message") + assert.equal(await message.textContent(), "Link!") +}) - this.assert.notOk(await this.formSubmitStarted) - } +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 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]") + await page.click("#link-method-inside-frame-with-confirmation") + await nextBeat() - this.assert.notOk(await this.formSubmitStarted) - } + assert.notOk(await hasSelector(page, "#frame div.message"), "Not confirming form submission does not submit the form") +}) - 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]") +test("test link method form submission outside frame", async ({ page }) => { + await page.click("#link-method-outside-frame") + await nextBody(page) - this.assert.ok(await this.formSubmitStarted) - } + const title = await page.locator("h1") + assert.equal(await title.textContent(), "Hello") +}) - 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")) - } +test("test stream link method form submission outside frame", async ({ page }) => { + await page.click("#stream-link-method-outside-frame") + await nextBeat() - 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")) - } + const message = page.locator("#frame div.message") + assert.equal(await message.textContent(), "Link!") +}) - async "test POST to external action ignored"() { - await this.clickSelector("#submit-external") - await this.noNextEventNamed("turbo:before-fetch-request") - await this.nextBody +test("test link method form submission within form outside frame", async ({ page }) => { + await page.click("#link-method-within-form-outside-frame") + await nextBody(page) - this.assert.equal(await this.location, "https://httpbin.org/post") - } + const title = await page.locator("h1") + assert.equal(await title.textContent(), "Hello") +}) - 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 +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() - this.assert.equal(await this.location, "https://httpbin.org/post") - } + assert.equal(await page.textContent("#frame div.message"), "Link!") +}) - 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 +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]") - this.assert.equal(await this.location, "https://httpbin.org/post") - } + assert.notOk(await formSubmitStarted(page)) +}) - get formSubmitStarted() { - return this.getFromLocalStorage("formSubmitStarted") - } +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]") - get formSubmitEnded() { - return this.getFromLocalStorage("formSubmitEnded") - } + assert.notOk(await formSubmitStarted(page)) +}) + +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]") + + assert.ok(await formSubmitStarted(page)) +}) + +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")) +}) + +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")) +}) + +test("test POST to external action ignored", async ({ page }) => { + await page.click("#submit-external") + await noNextEventNamed(page, "turbo:before-fetch-request") + await nextBody(page) + + assert.equal(page.url(), "https://httpbin.org/post") +}) + +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) + + assert.equal(page.url(), "https://httpbin.org/post") +}) + +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) + + assert.equal(page.url(), "https://httpbin.org/post") +}) + +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 9ef2ff199..0f0ec4045 100644 --- a/src/tests/functional/frame_tests.ts +++ b/src/tests/functional/frame_tests.ts @@ -1,676 +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") - } - - 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") - } - - 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") - } - - 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") - } - - 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]") - } - - 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") - } - - 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") + 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 { @@ -678,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 abd11af32..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,131 +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")) - } +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/loading.html") + await readEventLogs(page) +}) - async "test lazy loading within a details element"() { - await this.nextBeat +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") +}) - const frameContents = "#loading-lazy turbo-frame h2" - this.assert.notOk(await this.hasSelector(frameContents)) +test("test lazy loading within a details element", async ({ page }) => { + await nextBeat() - await this.clickSelector("#loading-lazy summary") - await this.nextBeat + const frameContents = "#loading-lazy turbo-frame h2" + assert.notOk(await hasSelector(page, frameContents)) + assert.ok(await hasSelector(page, "#loading-lazy turbo-frame:not([complete])")) - const contents = await this.querySelector(frameContents) - this.assert.equal(await contents.getVisibleText(), "Hello from a frame") - } + await page.click("#loading-lazy summary") + await nextBeat() - async "test changing loading attribute from lazy to eager loads frame"() { - const frameContents = "#loading-lazy turbo-frame h2" - 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") +}) - this.assert.notOk(await this.hasSelector(frameContents)) +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-lazy turbo-frame")?.setAttribute("loading", "eager") - ) - await this.nextBeat + assert.notOk(await hasSelector(page, frameContents)) - const contents = await this.querySelector(frameContents) - await this.clickSelector("#loading-lazy summary") - this.assert.equal(await contents.getVisibleText(), "Hello from a frame") - } + await page.evaluate(() => document.querySelector("#loading-lazy turbo-frame")?.setAttribute("loading", "eager")) + await nextBeat() - async "test navigating a visible frame with loading=lazy navigates"() { - await this.clickSelector("#loading-lazy summary") - await this.nextBeat + const contents = await page.locator(frameContents) + await page.click("#loading-lazy summary") + assert.equal(await contents.textContent(), "Hello from a frame") +}) - const initialContents = await this.querySelector("#hello h2") - this.assert.equal(await initialContents.getVisibleText(), "Hello from a frame") +test("test navigating a visible frame with loading=lazy navigates", async ({ page }) => { + await page.click("#loading-lazy summary") + await nextBeat() - await this.clickSelector("#hello a") - await this.nextBeat + const initialContents = await page.locator("#hello h2") + assert.equal(await initialContents.textContent(), "Hello from a frame") - const navigatedContents = await this.querySelector("#hello h2") - this.assert.equal(await navigatedContents.getVisibleText(), "Frames: #hello") - } + await page.click("#hello a") + await nextBeat() - async "test changing src attribute on a frame with loading=lazy defers navigation"() { - const frameContents = "#loading-lazy turbo-frame h2" - await this.nextBeat + const navigatedContents = await page.locator("#hello h2") + assert.equal(await navigatedContents.textContent(), "Frames: #hello") +}) - await this.remote.execute(() => - document.querySelector("#loading-lazy turbo-frame")?.setAttribute("src", "/src/tests/fixtures/frames.html") - ) - this.assert.notOk(await this.hasSelector(frameContents)) +test("test changing src attribute on a frame with loading=lazy defers navigation", async ({ page }) => { + const frameContents = "#loading-lazy turbo-frame h2" + await nextBeat() - await this.clickSelector("#loading-lazy summary") - await this.nextBeat + await page.evaluate(() => + document.querySelector("#loading-lazy turbo-frame")?.setAttribute("src", "/src/tests/fixtures/frames.html") + ) + assert.notOk(await hasSelector(page, frameContents)) - const contents = await this.querySelector(frameContents) - this.assert.equal(await contents.getVisibleText(), "Frames: #hello") - } + await page.click("#loading-lazy summary") + await nextBeat() - async "test changing src attribute on a frame with loading=eager navigates"() { - const frameContents = "#loading-eager turbo-frame h2" - await this.nextBeat + const contents = await page.locator(frameContents) + assert.equal(await contents.textContent(), "Frames: #hello") +}) - await this.remote.execute(() => - document.querySelector("#loading-eager turbo-frame")?.setAttribute("src", "/src/tests/fixtures/frames.html") - ) +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-eager summary") - await this.nextBeat + await page.evaluate(() => + document.querySelector("#loading-eager turbo-frame")?.setAttribute("src", "/src/tests/fixtures/frames.html") + ) - const contents = await this.querySelector(frameContents) - this.assert.equal(await contents.getVisibleText(), "Frames: #frame") - } + await page.click("#loading-eager summary") + await nextBeat() - async "test reloading a frame reloads the content"() { - await this.nextBeat + const contents = await page.locator(frameContents) + assert.equal(await contents.textContent(), "Frames: #frame") +}) - await this.clickSelector("#loading-eager summary") - await this.nextBeat +test("test reloading a frame reloads the content", async ({ page }) => { + await nextBeat() - const frameContent = "#loading-eager turbo-frame#frame h2" - this.assert.ok(await this.hasSelector(frameContent)) - await this.remote.execute(() => (document.querySelector("#loading-eager turbo-frame") as any)?.reload()) - this.assert.ok(await this.hasSelector(frameContent)) - } - - async "test navigating away from a page does not reload its frames"() { - await this.clickSelector("#one") - await this.nextBody - - const eventLogs = await this.eventLogChannel.read() - const requestLogs = eventLogs.filter(([name]) => name == "turbo:before-fetch-request") - this.assert.equal(requestLogs.length, 1) - } - - 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 6f6a21f09..be1aa8e2d 100644 --- a/src/tests/functional/stream_tests.ts +++ b/src/tests/functional/stream_tests.ts @@ -1,39 +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!") +}) -StreamTests.registerSuite() +test("test receiving a stream message asynchronously", async ({ page }) => { + let messages = await page.locator("#messages > *").allTextContents() + + assert.ok(messages[0]) + assert.notOk(messages[1], "receives streams when connected") + assert.notOk(messages[2], "receives streams when connected") + + await page.click("#async button") + await nextBeat() + + messages = await page.locator("#messages > *").allTextContents() + + assert.ok(messages[0]) + assert.ok(messages[1], "receives streams when connected") + assert.notOk(messages[2], "receives streams when connected") + + await page.evaluate(() => document.getElementById("stream-source")?.remove()) + await nextBeat() + + await page.click("#async button") + await nextBeat() + + messages = await page.locator("#messages > *").allTextContents() + + 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 de3e32524..b5603121d 100644 --- a/src/tests/functional/visit_tests.ts +++ b/src/tests/functional/visit_tests.ts @@ -1,151 +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") - - 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 { @@ -154,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 05ba21394..0acc08560 100644 --- a/src/tests/runner.js +++ b/src/tests/runner.js @@ -2,6 +2,7 @@ const { TestServer } = require("../../dist/tests/server") const configuration = require("../../intern.json") const intern = require("intern").default const arg = require("arg"); +const { CHROMEVER } = process.env const args = arg({ "--grep": String, @@ -11,6 +12,14 @@ const args = arg({ intern.configure(configuration) intern.configure({ reporters: [ "runner" ] }) +if (CHROMEVER) { + intern.configure({ + tunnelOptions: { + drivers: [{ name: "chrome", version: CHROMEVER }] + } + }) +} + if (args["--grep"]) { intern.configure({ grep: args["--grep"] }) } @@ -24,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/src/tests/server.ts b/src/tests/server.ts index 8a2fa6890..49a292e8f 100644 --- a/src/tests/server.ts +++ b/src/tests/server.ts @@ -10,7 +10,7 @@ const streamResponses: Set = new Set() router.use(multer().none()) router.use((request, response, next) => { - if (request.accepts(["text/html", "application/xhtml+xml"])) { + if (request.accepts(["text/html", "application/xhtml+xml", "text/event-stream"])) { next() } else { response.sendStatus(422) diff --git a/src/util.ts b/src/util.ts index 5c4bd1b6e..84bb0de31 100644 --- a/src/util.ts +++ b/src/util.ts @@ -95,3 +95,7 @@ export function clearBusyState(...elements: Element[]) { element.removeAttribute("aria-busy") } } + +export function attributeTrue(element: Element, attributeName: string) { + return element.getAttribute(attributeName) === "true" +} 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==