From 5109c56fc738376a33ea79f14681356339e5224a Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Sun, 19 Jun 2022 09:55:36 +0100 Subject: [PATCH 01/14] Use `replaceChildren` in StreamActions.update (#534) * Use `replaceChildren` in StreamActions.update While processing `` elements, Turbo combines two steps: 1. sets [`innerHTML = ""`][innerHTML] 2. call [`append(this.templateContent)`][append] Modern `Element` implementations provide a [replaceChildren][] method that effectively combines these two steps into a single, atomic operation. The `Element.replaceChildren()` method [isn't supported by Internet Explorer][replaceChildren compatibility], but the `Element.append()` method that it replaces [has the same incompatibility issues][append compatibility]. [innerHTML]: https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML [append]: https://developer.mozilla.org/en-US/docs/Web/API/Element/append [append compatibility]: https://developer.mozilla.org/en-US/docs/Web/API/Element/append#browser_compatibility [replaceChildren]: https://developer.mozilla.org/en-US/docs/Web/API/Element/replaceChildren [replaceChildren compatibility]: https://developer.mozilla.org/en-US/docs/Web/API/Element/replaceChildren#browser_compatibility * Fix lint violation Co-authored-by: David Heinemeier Hansson --- src/core/streams/stream_actions.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/core/streams/stream_actions.ts b/src/core/streams/stream_actions.ts index 9cf84f646..62667e700 100644 --- a/src/core/streams/stream_actions.ts +++ b/src/core/streams/stream_actions.ts @@ -30,9 +30,6 @@ export const StreamActions: { }, update() { - this.targetElements.forEach((e) => { - e.innerHTML = "" - e.append(this.templateContent) - }) + this.targetElements.forEach((e) => e.replaceChildren(this.templateContent)) }, } From d0125a566890c3218a799861fefd741302fc4390 Mon Sep 17 00:00:00 2001 From: Darin Haener Date: Sun, 19 Jun 2022 02:13:13 -0700 Subject: [PATCH 02/14] Defensively create custom turbo elements (#483) There are instances when turbo attempts to create the frame and stream elements when they have already been registered. This is easily reproducible in development when using webpacker by following these steps: * Make a change to any file that webpacker compiles. * Click a turbo enabled link. * Boom. When Turbo replaces the page it appears as if it reloads the Turbo library and attempts to create the custom elements again. I have also observed this behavior in my production app via my exception monitoring service, but have been unable to reproduce this behavior in production on my own. This change simply checks for the existence of the custom element prior to to attempting to define it. The `get` method used here explicitly returns `undefined` when the element does not yet exist so it is safe to use the strict equality operator here. Reference: https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/get --- src/elements/index.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/elements/index.ts b/src/elements/index.ts index 34ae5410a..b5683ac25 100644 --- a/src/elements/index.ts +++ b/src/elements/index.ts @@ -7,5 +7,10 @@ FrameElement.delegateConstructor = FrameController export * from "./frame_element" export * from "./stream_element" -customElements.define("turbo-frame", FrameElement) -customElements.define("turbo-stream", StreamElement) +if (customElements.get("turbo-frame") === undefined) { + customElements.define("turbo-frame", FrameElement) +} + +if (customElements.get("turbo-stream") === undefined) { + customElements.define("turbo-stream", StreamElement) +} From f307b8d39673902f15cc28f9ca55953d2b905dcb Mon Sep 17 00:00:00 2001 From: Stephen Sugden Date: Sun, 19 Jun 2022 11:32:14 +0200 Subject: [PATCH 03/14] Do not declare global types/constants (#524) The submit-event polyfill was being interpreted by TSC as a script and not a module. This caused all the declarations there to be treated as global in the exported types. --- src/polyfills/submit-event.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/polyfills/submit-event.ts b/src/polyfills/submit-event.ts index 865f95dfc..5c5d0edde 100644 --- a/src/polyfills/submit-event.ts +++ b/src/polyfills/submit-event.ts @@ -41,3 +41,6 @@ function clickCaptured(event: Event) { }, }) })() + +// Ensure TypeScript parses this file as a module +export {} From 700c921838d7e22b18c19200cfa6cc39cb4ad09d Mon Sep 17 00:00:00 2001 From: mfo Date: Sun, 19 Jun 2022 12:12:37 +0200 Subject: [PATCH 04/14] fix(ie/edge): form.method='delete', raises Invalid argument. (#586) --- src/core/session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/session.ts b/src/core/session.ts index 770112393..49ecaabf0 100644 --- a/src/core/session.ts +++ b/src/core/session.ts @@ -168,7 +168,7 @@ export class Session if (linkMethod) { const form = document.createElement("form") - form.method = linkMethod + form.setAttribute("method", linkMethod) form.action = link.getAttribute("href") || "undefined" form.hidden = true From 4a61c68044099636bc6e7683267e053a7bd8bee6 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Sun, 19 Jun 2022 12:03:09 -0400 Subject: [PATCH 05/14] Expose Frame load state via `[complete]` attribute (#487) * Expose Frame load state via `[loaded]` attribute Closes https://github.com/hotwired/turbo/issues/429 --- Introduce the `` boolean attribute. The attribute's absence indicates that the frame has not yet been loaded, and is ready to be navigated. Its presence means that the contents of the frame have been fetch from its `[src]` attribute. Encoding the load state into the element's HTML aims to integrate with Snapshot caching. Once a frame is loaded, navigating away and then restoring a page's state from an Historical Snapshot should preserve the fact that the contents are already loaded. For both `eager` and `lazy` loaded frames, changing the element's `[src]` attribute (directly via JavaScript, or by clicking an `` element or submitting a `
` element) will remove the `[loaded]` attribute. Eager-loaded frames will immediately initiate a request to fetch the contents, and Lazy-loaded frames will initiate the request once they enter the viewport, or are changed to be eager-loading. When the `[src]` attribute is changed, the `FrameController` will only remove the `[loaded]` attribute if the element [isConnected][] to the document, so that the `[loaded]` attribute is not modified prior to Snapshot Caching or when re-mounting a Cached Snapshot. The act of "reloading" involves the removal of the `[loaded]` attribute, which can be done either by `FrameElement.reload()` or `document.getElementById("frame-element").removeAttribute("loaded")`. A side-effect of introducing the `[loaded]` attribute is that the `FrameController` no longer needs to internally track: 1. how the internal `currentURL` value compares to the external `sourceURL` value 2. whether or not the frame is "reloadable" By no longer tracking the `sourceURL` and `currentURL` separately, the implementation for the private `loadSourceURL` method can be simplified. Since there is no longer a `currentURL` property to rollback, the `try { ... } catch (error) { ... }` can be omitted, and the `this.sourceURL` presence check can be incorporated into the rest of the guard conditional. Finally, this commit introduce the `isIgnoringChangesTo()` and `ignoringChangesToAttribute()` private methods to disable FrameController observations for a given period of time. For example, when setting the `` attribute, previous implementation would set, then check the value of a `this.settingSourceURL` property to decide whether or not to fire attribute change callback code. This commit refines that pattern to support any property of the `FrameElement` that's returned from the `FrameElement.observedAttributes` static property, including the `"src"` or `"loaded"` value. When making internal modifications to those values, it's important to temporarily disable observation callbacks to avoid unnecessary requests and to limit the potential for infinitely recursing loops. [isConnected]: https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected * Expose Frame load state via `[complete]` attribute Closes https://github.com/hotwired/turbo/issues/429 --- Introduce the `` boolean attribute. The attribute's absence indicates that the frame has not yet been loaded, and is ready to be navigated. Its presence means that the contents of the frame have been fetch from its `[src]` attribute. Encoding the load state into the element's HTML aims to integrate with Snapshot caching. Once a frame is loaded, navigating away and then restoring a page's state from an Historical Snapshot should preserve the fact that the contents are already loaded. For both `eager` and `lazy` loaded frames, changing the element's `[src]` attribute (directly via JavaScript, or by clicking an `` element or submitting a `` element) will remove the `[complete]` attribute. Eager-loaded frames will immediately initiate a request to fetch the contents, and Lazy-loaded frames will initiate the request once they enter the viewport, or are changed to be eager-loading. When the `[src]` attribute is changed, the `FrameController` will only remove the `[complete]` attribute if the element [isConnected][] to the document, so that the `[complete]` attribute is not modified prior to Snapshot Caching or when re-mounting a Cached Snapshot. The act of "reloading" involves the removal of the `[complete]` attribute, which can be done either by `FrameElement.reload()` or `document.getElementById("frame-element").removeAttribute("complete")`. A side-effect of introducing the `[complete]` attribute is that the `FrameController` no longer needs to internally track: 1. how the internal `currentURL` value compares to the external `sourceURL` value 2. whether or not the frame is "reloadable" By no longer tracking the `sourceURL` and `currentURL` separately, the implementation for the private `loadSourceURL` method can be simplified. Since there is no longer a `currentURL` property to rollback, the `try { ... } catch (error) { ... }` can be omitted, and the `this.sourceURL` presence check can be incorporated into the rest of the guard conditional. Finally, this commit introduce the `isIgnoringChangesTo()` and `ignoringChangesToAttribute()` private methods to disable FrameController observations for a given period of time. For example, when setting the `` attribute, previous implementation would set, then check the value of a `this.settingSourceURL` property to decide whether or not to fire attribute change callback code. This commit refines that pattern to support any property of the `FrameController`, including the `"sourceURL"` or `"complete"` value. When making internal modifications to those values, it's important to temporarily disable observation callbacks to avoid unnecessary requests and to limit the potential for infinitely recursing loops. [isConnected]: https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected --- src/core/frames/frame_controller.ts | 105 ++++++++++++++------------ src/core/frames/frame_redirector.ts | 1 - src/elements/frame_element.ts | 10 ++- src/tests/fixtures/loading.html | 4 +- src/tests/functional/frame_tests.ts | 7 ++ src/tests/functional/loading_tests.ts | 76 +++++++++++++++++++ 6 files changed, 151 insertions(+), 52 deletions(-) diff --git a/src/core/frames/frame_controller.ts b/src/core/frames/frame_controller.ts index 85d514387..f6db0e875 100644 --- a/src/core/frames/frame_controller.ts +++ b/src/core/frames/frame_controller.ts @@ -1,4 +1,9 @@ -import { FrameElement, FrameElementDelegate, FrameLoadingStyle } from "../../elements/frame_element" +import { + FrameElement, + FrameElementDelegate, + FrameLoadingStyle, + FrameElementObservedAttribute, +} from "../../elements/frame_element" import { FetchMethod, FetchRequest, FetchRequestDelegate, FetchRequestHeaders } from "../../http/fetch_request" import { FetchResponse } from "../../http/fetch_response" import { AppearanceObserver, AppearanceObserverDelegate } from "../../observers/appearance_observer" @@ -29,14 +34,13 @@ export class FrameController readonly appearanceObserver: AppearanceObserver readonly linkInterceptor: LinkInterceptor readonly formInterceptor: FormInterceptor - currentURL?: string | null formSubmission?: FormSubmission fetchResponseLoaded = (_fetchResponse: FetchResponse) => {} 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) @@ -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/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/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/functional/frame_tests.ts b/src/tests/functional/frame_tests.ts index 9ef2ff199..a9afd1316 100644 --- a/src/tests/functional/frame_tests.ts +++ b/src/tests/functional/frame_tests.ts @@ -450,6 +450,7 @@ export class FrameTests extends TurboDriveTestCase { this.assert.equal(await title.getVisibleText(), "Frames") this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") + this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") } async "test navigating frame with a[data-turbo-action=advance] pushes URL state"() { @@ -464,6 +465,7 @@ export class FrameTests extends TurboDriveTestCase { this.assert.equal(await title.getVisibleText(), "Frames") this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") + this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") } async "test navigating frame with form[method=get][data-turbo-action=advance] pushes URL state"() { @@ -478,6 +480,7 @@ export class FrameTests extends TurboDriveTestCase { this.assert.equal(await title.getVisibleText(), "Frames") this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") + this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") } async "test navigating frame with form[method=get][data-turbo-action=advance] to the same URL clears the [aria-busy] and [data-turbo-preview] state"() { @@ -505,6 +508,7 @@ export class FrameTests extends TurboDriveTestCase { this.assert.equal(await title.getVisibleText(), "Frames") this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") + this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") } async "test navigating frame with form[method=post][data-turbo-action=advance] to the same URL clears the [aria-busy] and [data-turbo-preview] state"() { @@ -518,6 +522,7 @@ export class FrameTests extends TurboDriveTestCase { this.assert.equal(await this.attributeForSelector("#frame", "aria-busy"), null, "clears turbo-frame[aria-busy]") this.assert.equal(await this.attributeForSelector("#html", "aria-busy"), null, "clears html[aria-busy]") this.assert.equal(await this.attributeForSelector("#html", "data-turbo-preview"), null, "clears html[aria-busy]") + this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") } async "test navigating frame with button[data-turbo-action=advance] pushes URL state"() { @@ -532,6 +537,7 @@ export class FrameTests extends TurboDriveTestCase { this.assert.equal(await title.getVisibleText(), "Frames") this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") + this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") } async "test navigating back after pushing URL state from a turbo-frame[data-turbo-action=advance] restores the frames previous contents"() { @@ -567,6 +573,7 @@ export class FrameTests extends TurboDriveTestCase { this.assert.equal(await title.getVisibleText(), "Frames") this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") + this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") } async "test turbo:before-fetch-request fires on the frame element"() { diff --git a/src/tests/functional/loading_tests.ts b/src/tests/functional/loading_tests.ts index abd11af32..eb0c74ffc 100644 --- a/src/tests/functional/loading_tests.ts +++ b/src/tests/functional/loading_tests.ts @@ -14,6 +14,7 @@ export class LoadingTests extends TurboDriveTestCase { async "test eager loading within a details element"() { await this.nextBeat this.assert.ok(await this.hasSelector("#loading-eager turbo-frame#frame h2")) + this.assert.ok(await this.hasSelector("#loading-eager turbo-frame[complete]"), "has [complete] attribute") } async "test lazy loading within a details element"() { @@ -21,12 +22,14 @@ export class LoadingTests extends TurboDriveTestCase { const frameContents = "#loading-lazy turbo-frame h2" this.assert.notOk(await this.hasSelector(frameContents)) + this.assert.ok(await this.hasSelector("#loading-lazy turbo-frame:not([complete])")) await this.clickSelector("#loading-lazy summary") await this.nextBeat const contents = await this.querySelector(frameContents) this.assert.equal(await contents.getVisibleText(), "Hello from a frame") + this.assert.ok(await this.hasSelector("#loading-lazy turbo-frame[complete]"), "has [complete] attribute") } async "test changing loading attribute from lazy to eager loads frame"() { @@ -98,8 +101,11 @@ export class LoadingTests extends TurboDriveTestCase { const frameContent = "#loading-eager turbo-frame#frame h2" this.assert.ok(await this.hasSelector(frameContent)) + this.assert.ok(await this.hasSelector("#loading-eager turbo-frame[complete]"), "has [complete] attribute") + await this.remote.execute(() => (document.querySelector("#loading-eager turbo-frame") as any)?.reload()) this.assert.ok(await this.hasSelector(frameContent)) + this.assert.ok(await this.hasSelector("#loading-eager turbo-frame:not([complete])"), "clears [complete] attribute") } async "test navigating away from a page does not reload its frames"() { @@ -111,6 +117,76 @@ export class LoadingTests extends TurboDriveTestCase { this.assert.equal(requestLogs.length, 1) } + async "test removing the [complete] attribute of an eager frame reloads the content"() { + await this.nextEventOnTarget("frame", "turbo:frame-load") + await this.remote.execute(() => document.querySelector("#loading-eager turbo-frame")?.removeAttribute("complete")) + await this.nextEventOnTarget("frame", "turbo:frame-load") + + this.assert.ok( + await this.hasSelector("#loading-eager turbo-frame[complete]"), + "sets the [complete] attribute after re-loading" + ) + } + + async "test changing [src] attribute on a [complete] frame with loading=lazy defers navigation"() { + await this.nextEventOnTarget("frame", "turbo:frame-load") + await this.clickSelector("#loading-lazy summary") + await this.nextEventOnTarget("hello", "turbo:frame-load") + + this.assert.ok(await this.hasSelector("#loading-lazy turbo-frame[complete]"), "lazy frame is complete") + this.assert.equal(await (await this.querySelector("#hello h2")).getVisibleText(), "Hello from a frame") + + await this.clickSelector("#loading-lazy summary") + await this.clickSelector("#one") + await this.nextEventNamed("turbo:load") + await this.goBack() + await this.nextBody + await this.noNextEventNamed("turbo:frame-load") + + let src = new URL((await this.attributeForSelector("#hello", "src")) || "") + + this.assert.ok(await this.hasSelector("#loading-lazy turbo-frame[complete]"), "lazy frame is complete") + this.assert.equal(src.pathname, "/src/tests/fixtures/frames/hello.html", "lazy frame retains [src]") + + await this.clickSelector("#link-lazy-frame") + await this.noNextEventNamed("turbo:frame-load") + + this.assert.ok(await this.hasSelector("#loading-lazy turbo-frame:not([complete])"), "lazy frame is not complete") + + await this.clickSelector("#loading-lazy summary") + await this.nextEventOnTarget("hello", "turbo:frame-load") + + src = new URL((await this.attributeForSelector("#hello", "src")) || "") + + this.assert.equal( + await (await this.querySelector("#loading-lazy turbo-frame h2")).getVisibleText(), + "Frames: #hello" + ) + this.assert.ok(await this.hasSelector("#loading-lazy turbo-frame[complete]"), "lazy frame is complete") + this.assert.equal(src.pathname, "/src/tests/fixtures/frames.html", "lazy frame navigates") + } + + async "test navigating away from a page and then back does not reload its frames"() { + await this.clickSelector("#one") + await this.nextBody + await this.eventLogChannel.read() + await this.goBack() + await this.nextBody + + const eventLogs = await this.eventLogChannel.read() + const requestLogs = eventLogs.filter(([name]) => name == "turbo:before-fetch-request") + const requestsOnEagerFrame = requestLogs.filter((record) => record[2] == "frame") + const requestsOnLazyFrame = requestLogs.filter((record) => record[2] == "hello") + + this.assert.equal(requestsOnEagerFrame.length, 0, "does not reload eager frame") + this.assert.equal(requestsOnLazyFrame.length, 0, "does not reload lazy frame") + + await this.clickSelector("#loading-lazy summary") + await this.nextEventOnTarget("hello", "turbo:before-fetch-request") + await this.nextEventOnTarget("hello", "turbo:frame-render") + await this.nextEventOnTarget("hello", "turbo:frame-load") + } + async "test disconnecting and reconnecting a frame does not reload the frame"() { await this.nextBeat From 631701d59f5b52caf977ebb1bf990e2cf435ea38 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Sun, 19 Jun 2022 12:44:54 -0400 Subject: [PATCH 06/14] Turbo stream source (#415) * Improve test coverage for streams over SSE Add a `` element to the `src/tests/fixtures/stream.html` file so that tests can exercise receiving `` elements asynchronously over an `EventSource` instance that polls for Server-sent Events. Extend the `src/tests/server.ts` to account for the `Accept: text/event-stream` requests that are made by browsers via the `new EventSource(...)` instance. * Introduce `` Closes https://github.com/hotwired/turbo/issues/413 The `` element accepts a `[src]` attribute, and uses that to connect Turbo to poll for streams published on the server side. When the element is connected to the document, the stream source is connected. When the element is disconnected, the stream is disconnected. When declared with an `ws://` or `wss://` URL, the underlying Stream Source will be a `WebSocket` instance. Otherwise, the connection is through an `EventSource`. Since the document's `` is persistent across navigations, the `` is meant to be mounted within the `` element. Typical full page navigations driven by Turbo will result in the `` being discarded and replaced with the resulting document. It's the server's responsibility to ensure that the element is present on each page that requires streaming. * fix lint errors Co-authored-by: David Heinemeier Hansson --- src/elements/index.ts | 5 +++++ src/elements/stream_source_element.ts | 22 ++++++++++++++++++++ src/tests/fixtures/stream.html | 8 +++++++- src/tests/functional/stream_tests.ts | 29 +++++++++++++++++++++++++++ src/tests/server.ts | 2 +- 5 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 src/elements/stream_source_element.ts 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/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/functional/stream_tests.ts b/src/tests/functional/stream_tests.ts index 6f6a21f09..74acc3350 100644 --- a/src/tests/functional/stream_tests.ts +++ b/src/tests/functional/stream_tests.ts @@ -34,6 +34,35 @@ export class StreamTests extends FunctionalTestCase { this.assert.equal(await element[0].getVisibleText(), "Hello CSS!") this.assert.equal(await element[1].getVisibleText(), "Hello CSS!") } + + async "test receiving a stream message asynchronously"() { + let messages = await this.querySelectorAll("#messages > *") + + this.assert.ok(messages[0]) + this.assert.notOk(messages[1], "receives streams when connected") + this.assert.notOk(messages[2], "receives streams when connected") + + await this.clickSelector("#async button") + await this.nextBeat + + messages = await this.querySelectorAll("#messages > *") + + this.assert.ok(messages[0]) + this.assert.ok(messages[1], "receives streams when connected") + this.assert.notOk(messages[2], "receives streams when connected") + + await this.evaluate(() => document.getElementById("stream-source")?.remove()) + await this.nextBeat + + await this.clickSelector("#async button") + await this.nextBeat + + messages = await this.querySelectorAll("#messages > *") + + this.assert.ok(messages[0]) + this.assert.ok(messages[1], "receives streams when connected") + this.assert.notOk(messages[2], "does not receive streams when disconnected") + } } StreamTests.registerSuite() 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) From 50d8bd71c110c7ad50f11a797f5e78e8279435c5 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Wed, 22 Jun 2022 11:22:23 -0400 Subject: [PATCH 07/14] Support development ChromeDriver version overrides (#606) Refines the changes introduced to the CI configuration made in [b1cbe12][]. Recent Chrome upgrades are causing errors like the following while executing the test suite locally: ``` SessionNotCreatedException: [POST http://localhost:4444/wd/hub/session / {"desiredCapabilities":{"name":"intern","idle-timeout":60,"browserName":"chrome","goog:chromeOptions":{"args":["headless","disable-gpu","no-sandbox"]},"browser":"chrome"}}] session not created: This version of ChromeDriver only supports Chrome version 99 Current browser version is 102.0.5005.115 with binary path ``` The **only supports Chrome version 99** portion is due to `digdug`'s locked-support for Chrome version 99 declared in its [webdrivers.json][] file. This commit checks for the presence of the `CHROMEVER` environment variable, and overrides the [intern.json](./intern.json) configuration to incorporate that into its `tunnelOptions.drivers` value. [webdrivers.json]: https://github.com/theintern/digdug/blob/806dcf29c2265d3cb1a26a09ef5b43e93fc2a739/src/webdrivers.json#L8-L11 [b1cbe12]: https://github.com/hotwired/turbo/pull/551/commits/b1cbe123895259408d35b1c918ab2e9a51ba6683 --- .github/workflows/ci.yml | 5 +---- CONTRIBUTING.md | 3 +++ src/tests/runner.js | 9 +++++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efbfaa079..a307a8e68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,10 +24,7 @@ jobs: 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 81e372f98..c4e7170a1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,9 @@ Once you are done developing the feature or bug fix you have 2 options: ### 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. +To override the ChromeDriver version, declare the `CHROMEVER` environment +variable. + The tests are using the compiled version of the library and they are themselves also compiled. To compile the tests and library and watch for changes: ```bash diff --git a/src/tests/runner.js b/src/tests/runner.js index 05ba21394..7672b9d60 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"] }) } From 98cdc4037788f078a741c7fb50412db0101bcb97 Mon Sep 17 00:00:00 2001 From: Manuel Puyol Date: Wed, 22 Jun 2022 10:24:40 -0500 Subject: [PATCH 08/14] Only update history when Turbo visit is renderable (#601) * use location.replace instead of location.reload * update when we change the history * get location from visitStart * always pass the visit to renderError/Page * rollback name update * revert visit_control view hack * await nextBeat for the URL to be ready * remove console log --- src/core/drive/navigator.ts | 4 ++-- src/core/drive/page_view.ts | 8 ++++++-- src/core/drive/visit.ts | 6 +++--- src/core/native/browser_adapter.ts | 8 ++++++-- src/tests/fixtures/rendering.html | 1 + src/tests/fixtures/visit_control_reload.html | 15 ++++++++++++--- src/tests/functional/visit_tests.ts | 2 ++ 7 files changed, 32 insertions(+), 12 deletions(-) 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 ViewRendering
+

Visit control: reload - middle

Visit control: reload

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/visit_tests.ts b/src/tests/functional/visit_tests.ts index de3e32524..0b4da6f0f 100644 --- a/src/tests/functional/visit_tests.ts +++ b/src/tests/functional/visit_tests.ts @@ -10,6 +10,8 @@ export class VisitTests extends TurboDriveTestCase { const urlBeforeVisit = await this.location await this.visitLocation("/src/tests/fixtures/one.html") + await this.nextBeat + const urlAfterVisit = await this.location this.assert.notEqual(urlBeforeVisit, urlAfterVisit) this.assert.equal(await this.visitAction, "advance") From bdad71e4f4fbf01527cc2c64a883104a5f19af4b Mon Sep 17 00:00:00 2001 From: Kevin McConnell Date: Fri, 15 Jul 2022 00:57:08 +0100 Subject: [PATCH 09/14] Allow Turbo Streams w/ GET via `data-turbo-stream` (#612) Turbo Streams are normally supported only for [non-GET requests][0]. However there are cases where Turbo Streams responses to GET requests are useful. This commit adds the ability to use Turbo Streams with specific GET requests by setting `data-turbo-stream="true"` on a form or link. [0]: https://github.com/hotwired/turbo/pull/52 --- src/core/drive/form_submission.ts | 11 ++++++- src/core/frames/frame_controller.ts | 4 +-- src/core/session.ts | 16 ++++++---- src/tests/fixtures/form.html | 8 +++++ src/tests/functional/form_submission_tests.ts | 32 +++++++++++++++++++ src/util.ts | 4 +++ 6 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/core/drive/form_submission.ts b/src/core/drive/form_submission.ts index 460c9ba0e..56a1fc525 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 { @@ -153,6 +153,9 @@ export class FormSubmission { if (token) { headers["X-CSRF-Token"] = token } + } + + if (this.requestAcceptsTurboStreamResponse(request)) { headers["Accept"] = [StreamMessage.contentType, headers["Accept"]].join(", ") } } @@ -204,9 +207,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/frames/frame_controller.ts b/src/core/frames/frame_controller.ts index f6db0e875..b90808058 100644 --- a/src/core/frames/frame_controller.ts +++ b/src/core/frames/frame_controller.ts @@ -7,7 +7,7 @@ import { import { FetchMethod, FetchRequest, FetchRequestDelegate, FetchRequestHeaders } from "../../http/fetch_request" import { FetchResponse } from "../../http/fetch_response" import { AppearanceObserver, AppearanceObserverDelegate } from "../../observers/appearance_observer" -import { clearBusyState, getAttribute, parseHTMLDocument, markAsBusy } from "../../util" +import { clearBusyState, getAttribute, parseHTMLDocument, markAsBusy, attributeTrue } from "../../util" import { FormSubmission, FormSubmissionDelegate } from "../drive/form_submission" import { Snapshot } from "../snapshot" import { ViewDelegate } from "../view" @@ -149,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) diff --git a/src/core/session.ts b/src/core/session.ts index 49ecaabf0..a54231640 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" @@ -165,16 +165,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/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/functional/form_submission_tests.ts b/src/tests/functional/form_submission_tests.ts index e407d9055..bda7f0494 100644 --- a/src/tests/functional/form_submission_tests.ts +++ b/src/tests/functional/form_submission_tests.ts @@ -150,6 +150,14 @@ export class FormSubmissionTests extends TurboDriveTestCase { this.assert.equal(await this.getSearchParam("greeting"), "Hello from a form") } + async "test standard GET form submission with data-turbo-stream"() { + await this.clickSelector("#standard-get-form-with-stream-opt-in-submit") + + const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") + + this.assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) + } + async "test standard GET form submission events"() { await this.clickSelector("#standard-get-form-submit") @@ -800,6 +808,30 @@ export class FormSubmissionTests extends TurboDriveTestCase { this.assert.equal(await message.getVisibleText(), "Link!") } + async "test stream link GET method form submission inside frame"() { + await this.clickSelector("#stream-link-get-method-inside-frame") + + const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") + + this.assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) + } + + async "test stream link inside frame"() { + await this.clickSelector("#stream-link-inside-frame") + + const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") + + this.assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) + } + + async "test stream link outside frame"() { + await this.clickSelector("#stream-link-outside-frame") + + const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") + + this.assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) + } + async "test link method form submission within form inside frame"() { await this.clickSelector("#stream-link-method-within-form-inside-frame") await this.nextBeat diff --git a/src/util.ts b/src/util.ts index 0f5459a79..87a9550fa 100644 --- a/src/util.ts +++ b/src/util.ts @@ -92,3 +92,7 @@ export function clearBusyState(...elements: Element[]) { element.removeAttribute("aria-busy") } } + +export function attributeTrue(element: Element, attributeName: string) { + return element.getAttribute(attributeName) === "true" +} From 15e45dad8012702ddbbc9235e0ba4852dbfdf100 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Fri, 15 Jul 2022 18:30:36 -0400 Subject: [PATCH 10/14] Drive Browser tests with `playwright` (#609) Replaces the [intern][]-powered functional test suite with one powered by [playwright][]. The majority of the changes made aim to preserve the structure and method names of the original `intern` suite. There are some out-of-the-box Playwright features that could replace many of our bespoke helpers, but that work should be done after the migration. As a result, the majority of the changes involve a combination of: * replacing `async "test ..."() { ... }` with `test("test ...", async ({ page }) => { ... }` * replacing calls in the style of `this.methodName` with calls in the style of `methodName(page)` In some cases, exceptions were made. For example, most calls to `this.clickSelector` were replaced with calls to [page.click][]. The unit test suite --- Playwright's focus on a browser-powered test harness make it an odd fit for our small suite of "unit" tests. Unlike their browser-test counterparts, the unit tests do not have a history of flakiness. This changeset defers migrating those tests off of `intern` for a later batch of work. [intern]: https://theintern.io [playwright]: https://playwright.dev [page.click]: https://playwright.dev/docs/api/class-page#page-click --- .github/workflows/ci.yml | 19 +- CONTRIBUTING.md | 52 +- intern.json | 1 - package.json | 11 +- playwright.config.ts | 27 + rollup.config.js | 22 - src/tests/fixtures/test.js | 20 +- src/tests/functional/async_script_tests.ts | 32 +- src/tests/functional/autofocus_tests.ts | 138 +- src/tests/functional/cache_observer_tests.ts | 30 +- src/tests/functional/drive_disabled_tests.ts | 80 +- src/tests/functional/drive_tests.ts | 49 +- src/tests/functional/form_submission_tests.ts | 1780 +++++++++-------- .../functional/frame_navigation_tests.ts | 37 +- src/tests/functional/frame_tests.ts | 1344 +++++++------ src/tests/functional/import_tests.ts | 21 +- src/tests/functional/index.ts | 18 - src/tests/functional/loading_tests.ts | 365 ++-- src/tests/functional/navigation_tests.ts | 681 ++++--- .../functional/pausable_rendering_tests.ts | 53 +- .../functional/pausable_requests_tests.ts | 57 +- src/tests/functional/preloader_tests.ts | 108 +- src/tests/functional/rendering_tests.ts | 648 +++--- .../functional/scroll_restoration_tests.ts | 54 +- src/tests/functional/stream_tests.ts | 95 +- src/tests/functional/visit_tests.ts | 275 ++- src/tests/helpers/functional_test_case.ts | 179 -- src/tests/helpers/page.ts | 241 +++ src/tests/helpers/remote_channel.ts | 36 - src/tests/helpers/turbo_drive_test_case.ts | 111 - src/tests/runner.js | 8 - tsconfig.json | 2 +- yarn.lock | 1479 ++++++++------ 33 files changed, 4125 insertions(+), 3948 deletions(-) create mode 100644 playwright.config.ts delete mode 100644 src/tests/functional/index.ts delete mode 100644 src/tests/helpers/functional_test_case.ts create mode 100644 src/tests/helpers/page.ts delete mode 100644 src/tests/helpers/remote_channel.ts delete mode 100644 src/tests/helpers/turbo_drive_test_case.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a307a8e68..649ffd8f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ jobs: runs-on: ubuntu-latest + steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 @@ -18,6 +19,7 @@ jobs: key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - run: yarn install + - run: yarn run playwright install --with-deps - run: yarn build - name: Set Chrome Version @@ -29,8 +31,21 @@ jobs: - name: Lint run: yarn lint - - name: Test - run: yarn test + - name: Unit Test + run: yarn test:unit + + - name: Chrome Test + run: yarn test:browser --project=chrome + + - name: Firefox Test + run: yarn test:browser --project=firefox + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v2 + with: + name: playwright-report + path: playwright-report - name: Publish dev build run: .github/scripts/publish-dev-build '${{ secrets.DEV_BUILD_GITHUB_TOKEN }}' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c4e7170a1..5106f6a85 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,21 +34,46 @@ Once you are done developing the feature or bug fix you have 2 options: 2. Run a local webserver and checkout your changes manually ### Testing -The library is tested by running the test suite (found in: `src/tests/*`) against headless browsers. The browsers are setup in `intern.json` check it out to see the used browser environments. +The library is tested by running the test suite (found in: `src/tests/*`) against headless browsers. The browsers are setup in [intern.json](./intern.json) and [playwright.config.ts](./playwright.config.ts). Check them out to see the used browser environments. To override the ChromeDriver version, declare the `CHROMEVER` environment variable. +First, install the drivers to test the suite in browsers: + +``bash +yarn playwright install --with-deps +``` + The tests are using the compiled version of the library and they are themselves also compiled. To compile the tests and library and watch for changes: ```bash yarn watch ``` -To run the tests: +To run the unit tests: + +```bash +yarn test:unit +``` + +To run the browser tests: + +```bash +yarn test:browser +``` + +To run the browser suite against a particular browser (one of +`chrome|firefox`), pass the value as the `--project=$BROWSER` flag: ```bash -yarn test +yarn test:browser --project=chrome +``` + +To run the browser tests in a "headed" browser, pass the `--headed` flag: + +```bash +yarn test:browser --project=chrome --headed ``` ### Test files @@ -58,14 +83,23 @@ The html files needed for the tests are stored in: `src/tests/fixtures/` ### Run single test -To focus on single test grep for it: -```javascript -yarn test --grep TEST_CASE_NAME +To focus on single test, pass its file path: + +```bas +yarn test:browser TEST_FILE ``` -Where the `TEST_CASE_NAME` is the name of test you want to run. For example: -```javascript -yarn test --grep 'triggers before-render and render events' +Where the `TEST_FILE` is the name of test you want to run. For example: + +```base +yarn test:browser src/tests/functional/drive_tests.ts +``` + +To execute a particular test, append `:LINE` where `LINE` is the line number of +the call to `test("...")`: + +```bash +yarn test:browser src/tests/functional/drive_tests.ts:11 ``` ### Local webserver diff --git a/intern.json b/intern.json index 60c07e97e..583314227 100644 --- a/intern.json +++ b/intern.json @@ -1,6 +1,5 @@ { "suites": "dist/tests/unit.js", - "functionalSuites": "dist/tests/functional.js", "environments": [ { "browserName": "chrome", diff --git a/package.json b/package.json index 1ae29835a..5a74fcfa5 100644 --- a/package.json +++ b/package.json @@ -35,12 +35,14 @@ "access": "public" }, "devDependencies": { + "@playwright/test": "^1.22.2", "@rollup/plugin-node-resolve": "13.1.3", "@rollup/plugin-typescript": "8.3.1", "@types/multer": "^1.4.5", "@typescript-eslint/eslint-plugin": "^5.20.0", "@typescript-eslint/parser": "^5.20.0", "arg": "^5.0.1", + "chai": "~4.3.4", "eslint": "^8.13.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.0.0", @@ -58,10 +60,15 @@ "build:win": "tsc --noEmit false --declaration true --emitDeclarationOnly true --outDir dist/types & rollup -c", "watch": "rollup -wc", "start": "node src/tests/runner.js serveOnly", - "test": "NODE_OPTIONS=--inspect node src/tests/runner.js", - "test:win": "SET NODE_OPTIONS=--inspect & node src/tests/runner.js", + "test": "yarn test:unit && yarn test:browser", + "test:browser": "playwright test", + "test:unit": "NODE_OPTIONS=--inspect node src/tests/runner.js", + "test:unit:win": "SET NODE_OPTIONS=--inspect & node src/tests/runner.js", "prerelease": "yarn build && git --no-pager diff && echo && npm pack --dry-run && echo && read -n 1 -p \"Look OK? Press any key to publish and commit v$npm_package_version\" && echo", "release": "npm publish && git commit -am \"$npm_package_name v$npm_package_version\" && git push", "lint": "eslint . --ext .ts" + }, + "engines": { + "node": ">= 14" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..2e8d2f27a --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,27 @@ +import { type PlaywrightTestConfig, devices } from "@playwright/test" + +const config: PlaywrightTestConfig = { + projects: [ + { + name: "chrome", + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + ], + testDir: "./src/tests/functional", + testMatch: /.*_tests\.ts/, + webServer: { + command: "yarn start", + url: "http://localhost:9000/src/tests/fixtures/test.js", + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI, + }, + use: { + baseURL: "http://localhost:9000/", + }, +} + +export default config diff --git a/rollup.config.js b/rollup.config.js index f5fa09df3..bca5ee055 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -30,28 +30,6 @@ export default [ } }, - { - input: "src/tests/functional/index.ts", - output: [ - { - file: "dist/tests/functional.js", - format: "cjs", - sourcemap: true - } - ], - plugins: [ - resolve(), - typescript() - ], - external: [ - "http", - "intern" - ], - watch: { - include: "src/tests/**" - } - }, - { input: "src/tests/unit/index.ts", output: [ diff --git a/src/tests/fixtures/test.js b/src/tests/fixtures/test.js index 369c0098c..fa98eb3d2 100644 --- a/src/tests/fixtures/test.js +++ b/src/tests/fixtures/test.js @@ -1,4 +1,22 @@ (function(eventNames) { + function serializeToChannel(object, returned = {}) { + for (const key in object) { + const value = object[key] + + if (value instanceof URL) { + returned[key] = value.toJSON() + } else if (value instanceof Element) { + returned[key] = value.outerHTML + } else if (typeof value == "object") { + returned[key] = serializeToChannel(value) + } else { + returned[key] = value + } + } + + return returned + } + window.eventLogs = [] for (var i = 0; i < eventNames.length; i++) { @@ -9,7 +27,7 @@ function eventListener(event) { const skipped = document.documentElement.getAttribute("data-skip-event-details") || "" - eventLogs.push([event.type, skipped.includes(event.type) ? {} : event.detail, event.target.id]) + eventLogs.push([event.type, serializeToChannel(skipped.includes(event.type) ? {} : event.detail), event.target.id]) } window.mutationLogs = [] diff --git a/src/tests/functional/async_script_tests.ts b/src/tests/functional/async_script_tests.ts index 025127c89..f96a6852a 100644 --- a/src/tests/functional/async_script_tests.ts +++ b/src/tests/functional/async_script_tests.ts @@ -1,20 +1,20 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" +import { readEventLogs, visitAction } from "../helpers/page" -export class AsyncScriptTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/async_script.html") - } +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/async_script.html") + await readEventLogs(page) +}) - async "test does not emit turbo:load when loaded asynchronously after DOMContentLoaded"() { - const events = await this.eventLogChannel.read() - this.assert.deepEqual(events, []) - } +test("test does not emit turbo:load when loaded asynchronously after DOMContentLoaded", async ({ page }) => { + const events = await readEventLogs(page) - async "test following a link when loaded asynchronously after DOMContentLoaded"() { - this.clickSelector("#async-link") - await this.nextBody - this.assert.equal(await this.visitAction, "advance") - } -} + assert.deepEqual(events, []) +}) -AsyncScriptTests.registerSuite() +test("test following a link when loaded asynchronously after DOMContentLoaded", async ({ page }) => { + await page.click("#async-link") + + assert.equal(await visitAction(page), "advance") +}) diff --git a/src/tests/functional/autofocus_tests.ts b/src/tests/functional/autofocus_tests.ts index 79c349386..03deea684 100644 --- a/src/tests/functional/autofocus_tests.ts +++ b/src/tests/functional/autofocus_tests.ts @@ -1,78 +1,80 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" +import { hasSelector, nextBeat } from "../helpers/page" -export class AutofocusTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/autofocus.html") - } +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/autofocus.html") +}) - async "test autofocus first autofocus element on load"() { - await this.nextBeat - this.assert.ok( - await this.hasSelector("#first-autofocus-element:focus"), - "focuses the first [autofocus] element on the page" - ) - this.assert.notOk( - await this.hasSelector("#second-autofocus-element:focus"), - "focuses the first [autofocus] element on the page" - ) - } +test("test autofocus first autofocus element on load", async ({ page }) => { + await nextBeat() + assert.ok( + await hasSelector(page, "#first-autofocus-element:focus"), + "focuses the first [autofocus] element on the page" + ) + assert.notOk( + await hasSelector(page, "#second-autofocus-element:focus"), + "focuses the first [autofocus] element on the page" + ) +}) - async "test autofocus first [autofocus] element on visit"() { - await this.goToLocation("/src/tests/fixtures/navigation.html") - await this.clickSelector("#autofocus-link") - await this.sleep(500) +test("test autofocus first [autofocus] element on visit", async ({ page }) => { + await page.goto("/src/tests/fixtures/navigation.html") + await page.click("#autofocus-link") + await nextBeat() - this.assert.ok( - await this.hasSelector("#first-autofocus-element:focus"), - "focuses the first [autofocus] element on the page" - ) - this.assert.notOk( - await this.hasSelector("#second-autofocus-element:focus"), - "focuses the first [autofocus] element on the page" - ) - } + assert.ok( + await hasSelector(page, "#first-autofocus-element:focus"), + "focuses the first [autofocus] element on the page" + ) + assert.notOk( + await hasSelector(page, "#second-autofocus-element:focus"), + "focuses the first [autofocus] element on the page" + ) +}) - async "test navigating a frame with a descendant link autofocuses [autofocus]:first-of-type"() { - await this.clickSelector("#frame-inner-link") - await this.nextBeat +test("test navigating a frame with a descendant link autofocuses [autofocus]:first-of-type", async ({ page }) => { + await page.click("#frame-inner-link") + await nextBeat() - this.assert.ok( - await this.hasSelector("#frames-form-first-autofocus-element:focus"), - "focuses the first [autofocus] element in frame" - ) - this.assert.notOk( - await this.hasSelector("#frames-form-second-autofocus-element:focus"), - "focuses the first [autofocus] element in frame" - ) - } + assert.ok( + await hasSelector(page, "#frames-form-first-autofocus-element:focus"), + "focuses the first [autofocus] element in frame" + ) + assert.notOk( + await hasSelector(page, "#frames-form-second-autofocus-element:focus"), + "focuses the first [autofocus] element in frame" + ) +}) - async "test navigating a frame with a link targeting the frame autofocuses [autofocus]:first-of-type"() { - await this.clickSelector("#frame-outer-link") - await this.nextBeat +test("test navigating a frame with a link targeting the frame autofocuses [autofocus]:first-of-type", async ({ + page, +}) => { + await page.click("#frame-outer-link") + await nextBeat() - this.assert.ok( - await this.hasSelector("#frames-form-first-autofocus-element:focus"), - "focuses the first [autofocus] element in frame" - ) - this.assert.notOk( - await this.hasSelector("#frames-form-second-autofocus-element:focus"), - "focuses the first [autofocus] element in frame" - ) - } + assert.ok( + await hasSelector(page, "#frames-form-first-autofocus-element:focus"), + "focuses the first [autofocus] element in frame" + ) + assert.notOk( + await hasSelector(page, "#frames-form-second-autofocus-element:focus"), + "focuses the first [autofocus] element in frame" + ) +}) - async "test navigating a frame with a turbo-frame targeting the frame autofocuses [autofocus]:first-of-type"() { - await this.clickSelector("#drives-frame-target-link") - await this.nextBeat +test("test navigating a frame with a turbo-frame targeting the frame autofocuses [autofocus]:first-of-type", async ({ + page, +}) => { + await page.click("#drives-frame-target-link") + await nextBeat() - this.assert.ok( - await this.hasSelector("#frames-form-first-autofocus-element:focus"), - "focuses the first [autofocus] element in frame" - ) - this.assert.notOk( - await this.hasSelector("#frames-form-second-autofocus-element:focus"), - "focuses the first [autofocus] element in frame" - ) - } -} - -AutofocusTests.registerSuite() + assert.ok( + await hasSelector(page, "#frames-form-first-autofocus-element:focus"), + "focuses the first [autofocus] element in frame" + ) + assert.notOk( + await hasSelector(page, "#frames-form-second-autofocus-element:focus"), + "focuses the first [autofocus] element in frame" + ) +}) diff --git a/src/tests/functional/cache_observer_tests.ts b/src/tests/functional/cache_observer_tests.ts index f3797d57e..edc923391 100644 --- a/src/tests/functional/cache_observer_tests.ts +++ b/src/tests/functional/cache_observer_tests.ts @@ -1,18 +1,16 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" +import { hasSelector, nextBody } from "../helpers/page" -export class CacheObserverTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/cache_observer.html") - } +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/cache_observer.html") +}) - async "test removes stale elements"() { - this.assert(await this.hasSelector("#flash")) - this.clickSelector("#link") - await this.nextBody - await this.goBack() - await this.nextBody - this.assert.notOk(await this.hasSelector("#flash")) - } -} - -CacheObserverTests.registerSuite() +test("test removes stale elements", async ({ page }) => { + assert(await hasSelector(page, "#flash")) + page.click("#link") + await nextBody(page) + await page.goBack() + await nextBody(page) + assert.notOk(await hasSelector(page, "#flash")) +}) diff --git a/src/tests/functional/drive_disabled_tests.ts b/src/tests/functional/drive_disabled_tests.ts index 42cf78c4f..b8a422636 100644 --- a/src/tests/functional/drive_disabled_tests.ts +++ b/src/tests/functional/drive_disabled_tests.ts @@ -1,36 +1,44 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" - -export class DriveDisabledTests extends TurboDriveTestCase { - path = "/src/tests/fixtures/drive_disabled.html" - - async setup() { - await this.goToLocation(this.path) - } - - async "test drive disabled by default; click normal link"() { - this.clickSelector("#drive_disabled") - await this.nextBody - this.assert.equal(await this.pathname, this.path) - this.assert.equal(await this.visitAction, "load") - } - - async "test drive disabled by default; click link inside data-turbo='true'"() { - this.clickSelector("#drive_enabled") - await this.nextBody - this.assert.equal(await this.pathname, this.path) - this.assert.equal(await this.visitAction, "advance") - } - - async "test drive disabled by default; submit form inside data-turbo='true'"() { - await this.setLocalStorageFromEvent("turbo:submit-start", "formSubmitted", "true") - - this.clickSelector("#no_submitter_drive_enabled a#requestSubmit") - await this.nextBody - this.assert.ok(await this.getFromLocalStorage("formSubmitted")) - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.visitAction, "advance") - this.assert.equal(await this.getSearchParam("greeting"), "Hello from a redirect") - } -} - -DriveDisabledTests.registerSuite() +import { test } from "@playwright/test" +import { assert } from "chai" +import { + getFromLocalStorage, + nextBody, + pathname, + searchParams, + setLocalStorageFromEvent, + visitAction, +} from "../helpers/page" + +const path = "/src/tests/fixtures/drive_disabled.html" + +test.beforeEach(async ({ page }) => { + await page.goto(path) +}) + +test("test drive disabled by default; click normal link", async ({ page }) => { + await page.click("#drive_disabled") + await nextBody(page) + + assert.equal(pathname(page.url()), path) + assert.equal(await visitAction(page), "load") +}) + +test("test drive disabled by default; click link inside data-turbo='true'", async ({ page }) => { + await page.click("#drive_enabled") + await nextBody(page) + + assert.equal(pathname(page.url()), path) + assert.equal(await visitAction(page), "advance") +}) + +test("test drive disabled by default; submit form inside data-turbo='true'", async ({ page }) => { + await setLocalStorageFromEvent(page, "turbo:submit-start", "formSubmitted", "true") + + await page.click("#no_submitter_drive_enabled a#requestSubmit") + await nextBody(page) + + assert.ok(await getFromLocalStorage(page, "formSubmitted")) + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(await visitAction(page), "advance") + assert.equal(await searchParams(page.url()).get("greeting"), "Hello from a redirect") +}) diff --git a/src/tests/functional/drive_tests.ts b/src/tests/functional/drive_tests.ts index bc660d302..26d840f59 100644 --- a/src/tests/functional/drive_tests.ts +++ b/src/tests/functional/drive_tests.ts @@ -1,31 +1,28 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" +import { nextBody, pathname, visitAction } from "../helpers/page" -export class DriveTests extends TurboDriveTestCase { - path = "/src/tests/fixtures/drive.html" +const path = "/src/tests/fixtures/drive.html" - async setup() { - await this.goToLocation(this.path) - } +test.beforeEach(async ({ page }) => { + await page.goto(path) +}) - async "test drive enabled by default; click normal link"() { - this.clickSelector("#drive_enabled") - await this.nextBody - this.assert.equal(await this.pathname, this.path) - this.assert.equal(await this.visitAction, "advance") - } +test("test drive enabled by default; click normal link", async ({ page }) => { + page.click("#drive_enabled") + await nextBody(page) + assert.equal(pathname(page.url()), path) +}) - async "test drive to external link"() { - this.clickSelector("#drive_enabled_external") - await this.nextBody - this.assert.equal(await this.remote.execute(() => window.location.href), "https://example.com/") - } +test("test drive to external link", async ({ page }) => { + page.click("#drive_enabled_external") + await nextBody(page) + assert.equal(await page.evaluate(() => window.location.href), "https://example.com/") +}) - async "test drive enabled by default; click link inside data-turbo='false'"() { - this.clickSelector("#drive_disabled") - await this.nextBody - this.assert.equal(await this.pathname, this.path) - this.assert.equal(await this.visitAction, "load") - } -} - -DriveTests.registerSuite() +test("test drive enabled by default; click link inside data-turbo='false'", async ({ page }) => { + page.click("#drive_disabled") + await nextBody(page) + assert.equal(pathname(page.url()), path) + assert.equal(await visitAction(page), "load") +}) diff --git a/src/tests/functional/form_submission_tests.ts b/src/tests/functional/form_submission_tests.ts index bda7f0494..cc713df85 100644 --- a/src/tests/functional/form_submission_tests.ts +++ b/src/tests/functional/form_submission_tests.ts @@ -1,965 +1,985 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" - -export class FormSubmissionTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/form.html") - await this.setLocalStorageFromEvent("turbo:submit-start", "formSubmitStarted", "true") - await this.setLocalStorageFromEvent("turbo:submit-end", "formSubmitEnded", "true") - } - - async "test standard form submission renders a progress bar"() { - await this.remote.execute(() => window.Turbo.setProgressBarDelay(0)) - await this.clickSelector("#standard form.sleep input[type=submit]") - - await this.waitUntilSelector(".turbo-progress-bar") - this.assert.ok(await this.hasSelector(".turbo-progress-bar"), "displays progress bar") - - await this.nextBody - await this.waitUntilNoSelector(".turbo-progress-bar") - - this.assert.notOk(await this.hasSelector(".turbo-progress-bar"), "hides progress bar") - } - - async "test form submission with confirmation confirmed"() { - await this.clickSelector("#standard form.confirm input[type=submit]") - - this.assert.equal(await this.getAlertText(), "Are you sure?") - await this.acceptAlert() - await this.nextEventNamed("turbo:load") - this.assert.ok(await this.formSubmitStarted) - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - } - - async "test form submission with confirmation cancelled"() { - await this.clickSelector("#standard form.confirm input[type=submit]") - - this.assert.equal(await this.getAlertText(), "Are you sure?") - await this.dismissAlert() - this.assert.notOk(await this.formSubmitStarted) - } - - async "test form submission with secondary submitter click - confirmation confirmed"() { - await this.clickSelector("#standard form.confirm #secondary_submitter") - - this.assert.equal(await this.getAlertText(), "Are you really sure?") - await this.acceptAlert() - await this.nextEventNamed("turbo:load") - this.assert.ok(await this.formSubmitStarted) - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "advance") - this.assert.equal(await this.getSearchParam("greeting"), "secondary_submitter") - } - - async "test form submission with secondary submitter click - confirmation cancelled"() { - await this.clickSelector("#standard form.confirm #secondary_submitter") - - this.assert.equal(await this.getAlertText(), "Are you really sure?") - await this.dismissAlert() - this.assert.notOk(await this.formSubmitStarted) - } - - async "test from submission with confirmation overriden"() { - await this.remote.execute(() => window.Turbo.setConfirmMethod(() => Promise.resolve(confirm("Overriden message")))) - - await this.clickSelector("#standard form.confirm input[type=submit]") - - this.assert.equal(await this.getAlertText(), "Overriden message") - await this.acceptAlert() - this.assert.ok(await this.formSubmitStarted) - } - - async "test standard form submission does not render a progress bar before expiring the delay"() { - await this.remote.execute(() => window.Turbo.setProgressBarDelay(500)) - await this.clickSelector("#standard form.redirect input[type=submit]") +import { Page, test } from "@playwright/test" +import { assert } from "chai" +import { + getFromLocalStorage, + getSearchParam, + hasSelector, + isScrolledToTop, + nextAttributeMutationNamed, + nextBeat, + nextBody, + nextEventNamed, + nextEventOnTarget, + noNextEventNamed, + outerHTMLForSelector, + pathname, + readEventLogs, + scrollToSelector, + search, + searchParams, + setLocalStorageFromEvent, + visitAction, + waitUntilSelector, + waitUntilNoSelector, +} from "../helpers/page" + +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/form.html") + await setLocalStorageFromEvent(page, "turbo:submit-start", "formSubmitStarted", "true") + await setLocalStorageFromEvent(page, "turbo:submit-end", "formSubmitEnded", "true") + await readEventLogs(page) +}) + +test("test standard form submission renders a progress bar", async ({ page }) => { + await page.evaluate(() => window.Turbo.setProgressBarDelay(0)) + await page.click("#standard form.sleep input[type=submit]") + + await waitUntilSelector(page, ".turbo-progress-bar") + assert.ok(await hasSelector(page, ".turbo-progress-bar"), "displays progress bar") + + await nextBody(page) + await waitUntilNoSelector(page, ".turbo-progress-bar") + + assert.notOk(await hasSelector(page, ".turbo-progress-bar"), "hides progress bar") +}) + +test("test form submission with confirmation confirmed", async ({ page }) => { + page.on("dialog", (alert) => { + assert.equal(alert.message(), "Are you sure?") + alert.accept() + }) + + await page.click("#standard form.confirm input[type=submit]") + + await nextEventNamed(page, "turbo:load") + assert.ok(await formSubmitStarted(page)) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") +}) + +test("test form submission with confirmation cancelled", async ({ page }) => { + page.on("dialog", (alert) => { + assert.equal(alert.message(), "Are you sure?") + alert.dismiss() + }) + await page.click("#standard form.confirm input[type=submit]") + + assert.notOk(await formSubmitStarted(page)) +}) + +test("test form submission with secondary submitter click - confirmation confirmed", async ({ page }) => { + page.on("dialog", (alert) => { + assert.equal(alert.message(), "Are you really sure?") + alert.accept() + }) + + await page.click("#standard form.confirm #secondary_submitter") + + await nextEventNamed(page, "turbo:load") + assert.ok(await formSubmitStarted(page)) + assert.equal(await pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "advance") + assert.equal(getSearchParam(page.url(), "greeting"), "secondary_submitter") +}) + +test("test form submission with secondary submitter click - confirmation cancelled", async ({ page }) => { + page.on("dialog", (alert) => { + assert.equal(alert.message(), "Are you really sure?") + alert.dismiss() + }) + + await page.click("#standard form.confirm #secondary_submitter") + + assert.notOk(await formSubmitStarted(page)) +}) + +test("test from submission with confirmation overriden", async ({ page }) => { + page.on("dialog", (alert) => { + assert.equal(alert.message(), "Overriden message") + alert.accept() + }) + + await page.evaluate(() => window.Turbo.setConfirmMethod(() => Promise.resolve(confirm("Overriden message")))) + await page.click("#standard form.confirm input[type=submit]") + + assert.ok(await formSubmitStarted(page)) +}) + +test("test standard form submission does not render a progress bar before expiring the delay", async ({ page }) => { + await page.evaluate(() => window.Turbo.setProgressBarDelay(500)) + await page.click("#standard form.redirect input[type=submit]") + + assert.notOk(await hasSelector(page, ".turbo-progress-bar"), "does not show progress bar before delay") +}) + +test("test standard form submission with redirect response", async ({ page }) => { + await page.click("#standard form.redirect input[type=submit]") + await nextBody(page) + + assert.ok(await formSubmitStarted(page)) + assert.equal(await pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(await visitAction(page), "advance") + assert.equal(getSearchParam(page.url(), "greeting"), "Hello from a redirect") + assert.equal( + await nextAttributeMutationNamed(page, "html", "aria-busy"), + "true", + "sets [aria-busy] on the document element" + ) + assert.equal( + await nextAttributeMutationNamed(page, "html", "aria-busy"), + null, + "removes [aria-busy] from the document element" + ) +}) + +test("test standard POST form submission events", async ({ page }) => { + await page.click("#standard-post-form-submit") + + assert.ok(await formSubmitStarted(page), "fires turbo:submit-start") + + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") + + assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) + + await nextEventNamed(page, "turbo:before-fetch-response") + + assert.ok(await formSubmitEnded(page), "fires turbo:submit-end") + + await nextEventNamed(page, "turbo:before-visit") + await nextEventNamed(page, "turbo:visit") + await nextEventNamed(page, "turbo:before-cache") + await nextEventNamed(page, "turbo:before-render") + await nextEventNamed(page, "turbo:render") + await nextEventNamed(page, "turbo:load") +}) + +test("test standard POST form submission merges values from both searchParams and body", async ({ page }) => { + await page.click("#form-action-post-redirect-self-q-b") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(getSearchParam(page.url(), "q"), "b") + assert.equal(getSearchParam(page.url(), "sort"), "asc") +}) + +test("test standard POST form submission toggles submitter [disabled] attribute", async ({ page }) => { + await page.click("#standard-post-form-submit") + + assert.equal( + await nextAttributeMutationNamed(page, "standard-post-form-submit", "disabled"), + "", + "sets [disabled] on the submitter" + ) + assert.equal( + await nextAttributeMutationNamed(page, "standard-post-form-submit", "disabled"), + null, + "removes [disabled] from the submitter" + ) +}) + +test("test standard GET form submission", async ({ page }) => { + await page.click("#standard form.greeting input[type=submit]") + await nextBody(page) + + assert.ok(await formSubmitStarted(page)) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "advance") + assert.equal(getSearchParam(page.url(), "greeting"), "Hello from a form") +}) + +test("test standard GET form submission with data-turbo-stream", async ({ page }) => { + await page.click("#standard-get-form-with-stream-opt-in-submit") + + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") + + assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) +}) + +test("test standard GET form submission events", async ({ page }) => { + await page.click("#standard-get-form-submit") + + assert.ok(await formSubmitStarted(page), "fires turbo:submit-start") + + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") + + assert.notOk(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) + + await nextEventNamed(page, "turbo:before-fetch-response") + + assert.ok(await formSubmitEnded(page), "fires turbo:submit-end") + + await nextEventNamed(page, "turbo:before-visit") + await nextEventNamed(page, "turbo:visit") + await nextEventNamed(page, "turbo:before-cache") + await nextEventNamed(page, "turbo:before-render") + await nextEventNamed(page, "turbo:render") + await nextEventNamed(page, "turbo:load") +}) + +test("test standard GET form submission does not incorporate the current page's URLSearchParams values into the submission", async ({ + page, +}) => { + await page.click("#form-action-self-sort") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(search(page.url()), "?sort=asc") + + await page.click("#form-action-none-q-a") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(search(page.url()), "?q=a", "navigates without omitted keys") +}) + +test("test standard GET form submission does not merge values into the [action] attribute", async ({ page }) => { + await page.click("#form-action-self-sort") + await nextBody(page) + + assert.equal(await pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(await search(page.url()), "?sort=asc") + + await page.click("#form-action-self-q-b") + await nextBody(page) + + assert.equal(await pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(await search(page.url()), "?q=b", "navigates without omitted keys") +}) + +test("test standard GET form submission omits the [action] value's URLSearchParams from the submission", async ({ + page, +}) => { + await page.click("#form-action-self-submit") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(search(page.url()), "") +}) + +test("test standard GET form submission toggles submitter [disabled] attribute", async ({ page }) => { + await page.click("#standard-get-form-submit") + + assert.equal( + await nextAttributeMutationNamed(page, "standard-get-form-submit", "disabled"), + "", + "sets [disabled] on the submitter" + ) + assert.equal( + await nextAttributeMutationNamed(page, "standard-get-form-submit", "disabled"), + null, + "removes [disabled] from the submitter" + ) +}) + +test("test standard GET form submission appending keys", async ({ page }) => { + await page.goto("/src/tests/fixtures/form.html?query=1") + await page.click("#standard form.conflicting-values input[type=submit]") + await nextBody(page) - this.assert.notOk(await this.hasSelector(".turbo-progress-bar"), "does not show progress bar before delay") - } - - async "test standard form submission with redirect response"() { - await this.clickSelector("#standard form.redirect input[type=submit]") - await this.nextBody - - this.assert.ok(await this.formSubmitStarted) - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.visitAction, "advance") - this.assert.equal(await this.getSearchParam("greeting"), "Hello from a redirect") - this.assert.equal( - await this.nextAttributeMutationNamed("html", "aria-busy"), - "true", - "sets [aria-busy] on the document element" - ) - this.assert.equal( - await this.nextAttributeMutationNamed("html", "aria-busy"), - null, - "removes [aria-busy] from the document element" - ) - } - - async "test standard POST form submission events"() { - await this.clickSelector("#standard-post-form-submit") - - this.assert.ok(await this.formSubmitStarted, "fires turbo:submit-start") + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(getSearchParam(page.url(), "query"), "2") +}) - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") - - this.assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) +test("test standard form submission with empty created response", async ({ page }) => { + const htmlBefore = await outerHTMLForSelector(page, "body") + const button = await page.locator("#standard form.created input[type=submit]") + await button.click() + await nextBeat() - await this.nextEventNamed("turbo:before-fetch-response") + const htmlAfter = await outerHTMLForSelector(page, "body") + assert.equal(htmlAfter, htmlBefore) +}) - this.assert.ok(await this.formSubmitEnded, "fires turbo:submit-end") +test("test standard form submission with empty no-content response", async ({ page }) => { + const htmlBefore = await outerHTMLForSelector(page, "body") + const button = await page.locator("#standard form.no-content input[type=submit]") + await button.click() + await nextBeat() + + const htmlAfter = await outerHTMLForSelector(page, "body") + assert.equal(htmlAfter, htmlBefore) +}) + +test("test standard POST form submission with multipart/form-data enctype", async ({ page }) => { + await page.click("#standard form[method=post][enctype] input[type=submit]") + await nextBeat() + + const enctype = getSearchParam(page.url(), "enctype") + assert.ok(enctype?.startsWith("multipart/form-data"), "submits a multipart/form-data request") +}) + +test("test standard GET form submission ignores enctype", async ({ page }) => { + await page.click("#standard form[method=get][enctype] input[type=submit]") + await nextBeat() + + const enctype = getSearchParam(page.url(), "enctype") + assert.notOk(enctype, "GET form submissions ignore enctype") +}) + +test("test standard POST form submission without an enctype", async ({ page }) => { + await page.click("#standard form[method=post].no-enctype input[type=submit]") + await nextBeat() + + const enctype = getSearchParam(page.url(), "enctype") + assert.ok( + enctype?.startsWith("application/x-www-form-urlencoded"), + "submits a application/x-www-form-urlencoded request" + ) +}) + +test("test no-action form submission with single parameter", async ({ page }) => { + await page.click("#no-action form.single input[type=submit]") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(getSearchParam(page.url(), "query"), "1") + + await page.click("#no-action form.single input[type=submit]") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(getSearchParam(page.url(), "query"), "1") + + await page.goto("/src/tests/fixtures/form.html?query=2") + await page.click("#no-action form.single input[type=submit]") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(getSearchParam(page.url(), "query"), "1") +}) + +test("test no-action form submission with multiple parameters", async ({ page }) => { + await page.goto("/src/tests/fixtures/form.html?query=2") + await page.click("#no-action form.multiple input[type=submit]") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.deepEqual(searchParams(page.url()).getAll("query"), ["1", "2"]) + + await page.click("#no-action form.multiple input[type=submit]") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.deepEqual(searchParams(page.url()).getAll("query"), ["1", "2"]) +}) + +test("test no-action form submission submitter parameters", async ({ page }) => { + await page.click("#no-action form.button-param [type=submit]") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(getSearchParam(page.url(), "query"), "1") + assert.deepEqual(searchParams(page.url()).getAll("button"), []) + + await page.click("#no-action form.button-param [type=submit]") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(getSearchParam(page.url(), "query"), "1") + assert.deepEqual(searchParams(page.url()).getAll("button"), []) +}) + +test("test submitter with blank formaction submits to the current page", async ({ page }) => { + await page.click("#blank-formaction button") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.ok(await hasSelector(page, "#blank-formaction"), "overrides form[action] navigation") +}) + +test("test input named action with no action attribute", async ({ page }) => { + await page.click("#action-input form.no-action [type=submit]") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.equal(getSearchParam(page.url(), "action"), "1") + assert.equal(getSearchParam(page.url(), "query"), "1") +}) + +test("test input named action with action attribute", async ({ page }) => { + await page.click("#action-input form.action [type=submit]") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(getSearchParam(page.url(), "action"), "1") + assert.equal(getSearchParam(page.url(), "query"), "1") +}) + +test("test invalid form submission with unprocessable entity status", async ({ page }) => { + await page.click("#reject form.unprocessable_entity input[type=submit]") + await nextBody(page) - await this.nextEventNamed("turbo:before-visit") - await this.nextEventNamed("turbo:visit") - await this.nextEventNamed("turbo:before-cache") - await this.nextEventNamed("turbo:before-render") - await this.nextEventNamed("turbo:render") - await this.nextEventNamed("turbo:load") - } + const title = await page.locator("h1") + assert.equal(await title.textContent(), "Unprocessable Entity", "renders the response HTML") + assert.notOk(await hasSelector(page, "#frame form.reject"), "replaces entire page") +}) - async "test standard POST form submission merges values from both searchParams and body"() { - await this.clickSelector("#form-action-post-redirect-self-q-b") - await this.nextBody +test("test invalid form submission with long form", async ({ page }) => { + await scrollToSelector(page, "#reject form.unprocessable_entity_with_tall_form input[type=submit]") + await page.click("#reject form.unprocessable_entity_with_tall_form input[type=submit]") + await nextBody(page) - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.getSearchParam("q"), "b") - this.assert.equal(await this.getSearchParam("sort"), "asc") - } - - async "test standard POST form submission toggles submitter [disabled] attribute"() { - await this.clickSelector("#standard-post-form-submit") - - this.assert.equal( - await this.nextAttributeMutationNamed("standard-post-form-submit", "disabled"), - "", - "sets [disabled] on the submitter" - ) - this.assert.equal( - await this.nextAttributeMutationNamed("standard-post-form-submit", "disabled"), - null, - "removes [disabled] from the submitter" - ) - } - - async "test standard GET form submission"() { - await this.clickSelector("#standard form.greeting input[type=submit]") - await this.nextBody - - this.assert.ok(await this.formSubmitStarted) - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "advance") - this.assert.equal(await this.getSearchParam("greeting"), "Hello from a form") - } - - async "test standard GET form submission with data-turbo-stream"() { - await this.clickSelector("#standard-get-form-with-stream-opt-in-submit") - - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") - - this.assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) - } - - async "test standard GET form submission events"() { - await this.clickSelector("#standard-get-form-submit") - - this.assert.ok(await this.formSubmitStarted, "fires turbo:submit-start") - - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") - - this.assert.notOk(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) - - await this.nextEventNamed("turbo:before-fetch-response") - - this.assert.ok(await this.formSubmitEnded, "fires turbo:submit-end") - - await this.nextEventNamed("turbo:before-visit") - await this.nextEventNamed("turbo:visit") - await this.nextEventNamed("turbo:before-cache") - await this.nextEventNamed("turbo:before-render") - await this.nextEventNamed("turbo:render") - await this.nextEventNamed("turbo:load") - } - - async "test standard GET form submission does not incorporate the current page's URLSearchParams values into the submission"() { - await this.clickSelector("#form-action-self-sort") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.search, "?sort=asc") - - await this.clickSelector("#form-action-none-q-a") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.search, "?q=a", "navigates without omitted keys") - } + const title = await page.locator("h1") + assert.equal(await title.textContent(), "Unprocessable Entity", "renders the response HTML") + assert(await isScrolledToTop(page), "page is scrolled to the top") + assert.notOk(await hasSelector(page, "#frame form.reject"), "replaces entire page") +}) + +test("test invalid form submission with server error status", async ({ page }) => { + assert(await hasSelector(page, "head > #form-fixture-styles")) + await page.click("#reject form.internal_server_error input[type=submit]") + await nextBody(page) + + const title = await page.locator("h1") + assert.equal(await title.textContent(), "Internal Server Error", "renders the response HTML") + assert.notOk(await hasSelector(page, "head > #form-fixture-styles"), "replaces head") + assert.notOk(await hasSelector(page, "#frame form.reject"), "replaces entire page") +}) - async "test standard GET form submission does not merge values into the [action] attribute"() { - await this.clickSelector("#form-action-self-sort") - await this.nextBody +test("test submitter form submission reads button attributes", async ({ page }) => { + const button = await page.locator("#submitter form button[type=submit][formmethod=post]") + await button.click() + await nextBody(page) - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.search, "?sort=asc") + assert.equal(pathname(page.url()), "/src/tests/fixtures/two.html") + assert.equal(await visitAction(page), "advance") +}) - await this.clickSelector("#form-action-self-q-b") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.search, "?q=b", "navigates without omitted keys") - } - - async "test standard GET form submission omits the [action] value's URLSearchParams from the submission"() { - await this.clickSelector("#form-action-self-submit") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.search, "") - } - - async "test standard GET form submission toggles submitter [disabled] attribute"() { - await this.clickSelector("#standard-get-form-submit") - - this.assert.equal( - await this.nextAttributeMutationNamed("standard-get-form-submit", "disabled"), - "", - "sets [disabled] on the submitter" - ) - this.assert.equal( - await this.nextAttributeMutationNamed("standard-get-form-submit", "disabled"), - null, - "removes [disabled] from the submitter" - ) - } - - async "test standard GET form submission appending keys"() { - await this.goToLocation("/src/tests/fixtures/form.html?query=1") - await this.clickSelector("#standard form.conflicting-values input[type=submit]") - await this.nextBody +test("test submitter POST form submission with multipart/form-data formenctype", async ({ page }) => { + await page.click("#submitter form[method=post]:not([enctype]) input[formenctype]") + await nextBeat() - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.getSearchParam("query"), "2") - } + const enctype = getSearchParam(page.url(), "enctype") + assert.ok(enctype?.startsWith("multipart/form-data"), "submits a multipart/form-data request") +}) - async "test standard form submission with empty created response"() { - const htmlBefore = await this.outerHTMLForSelector("body") - const button = await this.querySelector("#standard form.created input[type=submit]") - await button.click() - await this.nextBeat +test("test submitter GET submission from submitter with data-turbo-frame", async ({ page }) => { + await page.click("#submitter form[method=get] [type=submit][data-turbo-frame]") + await nextBeat() - const htmlAfter = await this.outerHTMLForSelector("body") - this.assert.equal(htmlAfter, htmlBefore) - } + const message = await page.locator("#frame div.message") + const title = await page.locator("h1") + assert.equal(await title.textContent(), "Form") + assert.equal(await message.textContent(), "Frame redirected") +}) - async "test standard form submission with empty no-content response"() { - const htmlBefore = await this.outerHTMLForSelector("body") - const button = await this.querySelector("#standard form.no-content input[type=submit]") - await button.click() - await this.nextBeat +test("test submitter POST submission from submitter with data-turbo-frame", async ({ page }) => { + await page.click("#submitter form[method=post] [type=submit][data-turbo-frame]") + await nextBeat() - const htmlAfter = await this.outerHTMLForSelector("body") - this.assert.equal(htmlAfter, htmlBefore) - } + const message = await page.locator("#frame div.message") + const title = await page.locator("h1") + assert.equal(await title.textContent(), "Form") + assert.equal(await message.textContent(), "Frame redirected") +}) + +test("test frame form GET submission from submitter with data-turbo-frame=_top", async ({ page }) => { + await page.click("#frame form[method=get] [type=submit][data-turbo-frame=_top]") + await nextBody(page) + + const title = await page.locator("h1") + assert.equal(await title.textContent(), "One") +}) + +test("test frame form POST submission from submitter with data-turbo-frame=_top", async ({ page }) => { + await page.click("#frame form[method=post] [type=submit][data-turbo-frame=_top]") + await nextBody(page) + + const title = await page.locator("h1") + assert.equal(await title.textContent(), "One") +}) + +test("test frame POST form targetting frame submission", async ({ page }) => { + await page.click("#targets-frame-post-form-submit") + + assert.ok(await formSubmitStarted(page), "fires turbo:submit-start") + + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") + + assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) + assert.equal("frame", fetchOptions.headers["Turbo-Frame"]) + + await nextEventNamed(page, "turbo:before-fetch-response") + + assert.ok(await formSubmitEnded(page), "fires turbo:submit-end") + + await nextEventNamed(page, "turbo:frame-render") + await nextEventNamed(page, "turbo:frame-load") + + const otherEvents = await readEventLogs(page) + assert.equal(otherEvents.length, 0, "no more events") + + const src = (await page.getAttribute("#frame", "src")) || "" + assert.equal(new URL(src).pathname, "/src/tests/fixtures/frames/frame.html") +}) + +test("test frame POST form targetting frame toggles submitter's [disabled] attribute", async ({ page }) => { + await page.click("#targets-frame-post-form-submit") + + assert.equal( + await nextAttributeMutationNamed(page, "targets-frame-post-form-submit", "disabled"), + "", + "sets [disabled] on the submitter" + ) + assert.equal( + await nextAttributeMutationNamed(page, "targets-frame-post-form-submit", "disabled"), + null, + "removes [disabled] from the submitter" + ) +}) + +test("test frame GET form targetting frame submission", async ({ page }) => { + await page.click("#targets-frame-get-form-submit") + + assert.ok(await formSubmitStarted(page), "fires turbo:submit-start") + + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") + + assert.notOk(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) + assert.equal("frame", fetchOptions.headers["Turbo-Frame"]) + + await nextEventNamed(page, "turbo:before-fetch-response") + + assert.ok(await formSubmitEnded(page), "fires turbo:submit-end") + + await nextEventNamed(page, "turbo:frame-render") + await nextEventNamed(page, "turbo:frame-load") + + const otherEvents = await readEventLogs(page) + assert.equal(otherEvents.length, 0, "no more events") + + const src = (await page.getAttribute("#frame", "src")) || "" + assert.equal(new URL(src).pathname, "/src/tests/fixtures/frames/frame.html") +}) + +test("test frame GET form targetting frame toggles submitter's [disabled] attribute", async ({ page }) => { + await page.click("#targets-frame-get-form-submit") + + assert.equal( + await nextAttributeMutationNamed(page, "targets-frame-get-form-submit", "disabled"), + "", + "sets [disabled] on the submitter" + ) + assert.equal( + await nextAttributeMutationNamed(page, "targets-frame-get-form-submit", "disabled"), + null, + "removes [disabled] from the submitter" + ) +}) + +test("test frame form GET submission from submitter referencing another frame", async ({ page }) => { + await page.click("#frame form[method=get] [type=submit][data-turbo-frame=hello]") + await nextBeat() + + const title = await page.locator("h1") + const frameTitle = await page.locator("#hello h2") + assert.equal(await frameTitle.textContent(), "Hello from a frame") + assert.equal(await title.textContent(), "Form") +}) + +test("test frame form POST submission from submitter referencing another frame", async ({ page }) => { + await page.click("#frame form[method=post] [type=submit][data-turbo-frame=hello]") + await nextBeat() + + const title = await page.locator("h1") + const frameTitle = await page.locator("#hello h2") + assert.equal(await frameTitle.textContent(), "Hello from a frame") + assert.equal(await title.textContent(), "Form") +}) + +test("test frame form submission with redirect response", async ({ page }) => { + const path = (await page.getAttribute("#frame form.redirect input[name=path]", "value")) || "" + const url = new URL(path, "http://localhost:9000") + url.searchParams.set("enctype", "application/x-www-form-urlencoded;charset=UTF-8") + + const button = await page.locator("#frame form.redirect input[type=submit]") + await button.click() + await nextBeat() + + const message = await page.locator("#frame div.message") + assert.notOk(await hasSelector(page, "#frame form.redirect")) + assert.equal(await message.textContent(), "Frame redirected") + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html", "does not redirect _top") + assert.notOk(search(page.url()), "does not redirect _top") + assert.equal(await page.getAttribute("#frame", "src"), url.href, "redirects the target frame") +}) + +test("test frame form submission toggles the ancestor frame's [aria-busy] attribute", async ({ page }) => { + await page.click("#frame form.redirect input[type=submit]") + await nextBeat() + + assert.equal(await nextAttributeMutationNamed(page, "frame", "busy"), "", "sets [busy] on the #frame") + assert.equal(await nextAttributeMutationNamed(page, "frame", "aria-busy"), "true", "sets [aria-busy] on the #frame") + assert.equal(await nextAttributeMutationNamed(page, "frame", "busy"), null, "removes [busy] from the #frame") + assert.equal( + await nextAttributeMutationNamed(page, "frame", "aria-busy"), + null, + "removes [aria-busy] from the #frame" + ) +}) + +test("test frame form submission toggles the target frame's [aria-busy] attribute", async ({ page }) => { + await page.click('#targets-frame form.frame [type="submit"]') + await nextBeat() + + assert.equal(await nextAttributeMutationNamed(page, "frame", "busy"), "", "sets [busy] on the #frame") + assert.equal(await nextAttributeMutationNamed(page, "frame", "aria-busy"), "true", "sets [aria-busy] on the #frame") + + const title = await page.locator("#frame h2") + assert.equal(await title.textContent(), "Frame: Loaded") + assert.equal(await nextAttributeMutationNamed(page, "frame", "busy"), null, "removes [busy] from the #frame") + assert.equal( + await nextAttributeMutationNamed(page, "frame", "aria-busy"), + null, + "removes [aria-busy] from the #frame" + ) +}) + +test("test frame form submission with empty created response", async ({ page }) => { + const htmlBefore = await outerHTMLForSelector(page, "#frame") + const button = await page.locator("#frame form.created input[type=submit]") + await button.click() + await nextBeat() + + const htmlAfter = await outerHTMLForSelector(page, "#frame") + assert.equal(htmlAfter, htmlBefore) +}) + +test("test frame form submission with empty no-content response", async ({ page }) => { + const htmlBefore = await outerHTMLForSelector(page, "#frame") + const button = await page.locator("#frame form.no-content input[type=submit]") + await button.click() + await nextBeat() + + const htmlAfter = await outerHTMLForSelector(page, "#frame") + assert.equal(htmlAfter, htmlBefore) +}) + +test("test frame form submission within a frame submits the Turbo-Frame header", async ({ page }) => { + await page.click("#frame form.redirect input[type=submit]") + + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") + + assert.ok(fetchOptions.headers["Turbo-Frame"], "submits with the Turbo-Frame header") +}) + +test("test invalid frame form submission with unprocessable entity status", async ({ page }) => { + await page.click("#frame form.unprocessable_entity input[type=submit]") + + assert.ok(await formSubmitStarted(page), "fires turbo:submit-start") + await nextEventNamed(page, "turbo:before-fetch-request") + await nextEventNamed(page, "turbo:before-fetch-response") + assert.ok(await formSubmitEnded(page), "fires turbo:submit-end") + await nextEventNamed(page, "turbo:frame-render") + await nextEventNamed(page, "turbo:frame-load") + + const otherEvents = await readEventLogs(page) + assert.equal(otherEvents.length, 0, "no more events") + + const title = await page.locator("#frame h2") + assert.ok(await hasSelector(page, "#reject form"), "only replaces frame") + assert.equal(await title.textContent(), "Frame: Unprocessable Entity") +}) + +test("test invalid frame form submission with internal server errror status", async ({ page }) => { + await page.click("#frame form.internal_server_error input[type=submit]") + + assert.ok(await formSubmitStarted(page), "fires turbo:submit-start") + await nextEventNamed(page, "turbo:before-fetch-request") + await nextEventNamed(page, "turbo:before-fetch-response") + assert.ok(await formSubmitEnded(page), "fires turbo:submit-end") + await nextEventNamed(page, "turbo:frame-render") + await nextEventNamed(page, "turbo:frame-load") + + const otherEvents = await readEventLogs(page) + assert.equal(otherEvents.length, 0, "no more events") + + assert.ok(await hasSelector(page, "#reject form"), "only replaces frame") + assert.equal(await page.textContent("#frame h2"), "Frame: Internal Server Error") +}) + +test("test frame form submission with stream response", async ({ page }) => { + const button = await page.locator("#frame form.stream[method=post] input[type=submit]") + await button.click() + await nextBeat() + + const message = await page.locator("#frame div.message") + assert.ok(await hasSelector(page, "#frame form.redirect")) + assert.equal(await message.textContent(), "Hello!") + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") + assert.notOk(await page.getAttribute("#frame", "src"), "does not change frame's src") +}) + +test("test frame form submission with HTTP verb other than GET or POST", async ({ page }) => { + await page.click("#frame form.put.stream input[type=submit]") + await nextBeat() + + assert.ok(await hasSelector(page, "#frame form.redirect")) + assert.equal(await page.textContent("#frame div.message"), "1: Hello!") + assert.equal(pathname(page.url()), "/src/tests/fixtures/form.html") +}) + +test("test frame form submission with [data-turbo=false] on the form", async ({ page }) => { + await page.click('#frame form[data-turbo="false"] input[type=submit]') + await waitUntilSelector(page, "#element-id") + + assert.notOk(await formSubmitStarted(page)) +}) + +test("test frame form submission with [data-turbo=false] on the submitter", async ({ page }) => { + await page.click('#frame form:not([data-turbo]) input[data-turbo="false"]') + await waitUntilSelector(page, "#element-id") - async "test standard POST form submission with multipart/form-data enctype"() { - await this.clickSelector("#standard form[method=post][enctype] input[type=submit]") - await this.nextBeat - - const enctype = await this.getSearchParam("enctype") - this.assert.ok(enctype?.startsWith("multipart/form-data"), "submits a multipart/form-data request") - } - - async "test standard GET form submission ignores enctype"() { - await this.clickSelector("#standard form[method=get][enctype] input[type=submit]") - await this.nextBeat - - const enctype = await this.getSearchParam("enctype") - this.assert.notOk(enctype, "GET form submissions ignore enctype") - } - - async "test standard POST form submission without an enctype"() { - await this.clickSelector("#standard form[method=post].no-enctype input[type=submit]") - await this.nextBeat - - const enctype = await this.getSearchParam("enctype") - this.assert.ok( - enctype?.startsWith("application/x-www-form-urlencoded"), - "submits a application/x-www-form-urlencoded request" - ) - } - - async "test no-action form submission with single parameter"() { - await this.clickSelector("#no-action form.single input[type=submit]") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.getSearchParam("query"), "1") - - await this.clickSelector("#no-action form.single input[type=submit]") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.getSearchParam("query"), "1") - - await this.goToLocation("/src/tests/fixtures/form.html?query=2") - await this.clickSelector("#no-action form.single input[type=submit]") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.getSearchParam("query"), "1") - } - - async "test no-action form submission with multiple parameters"() { - await this.goToLocation("/src/tests/fixtures/form.html?query=2") - await this.clickSelector("#no-action form.multiple input[type=submit]") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.deepEqual(await this.getAllSearchParams("query"), ["1", "2"]) - - await this.clickSelector("#no-action form.multiple input[type=submit]") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.deepEqual(await this.getAllSearchParams("query"), ["1", "2"]) - } - - async "test no-action form submission submitter parameters"() { - await this.clickSelector("#no-action form.button-param [type=submit]") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.getSearchParam("query"), "1") - this.assert.deepEqual(await this.getAllSearchParams("button"), []) - - await this.clickSelector("#no-action form.button-param [type=submit]") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.getSearchParam("query"), "1") - this.assert.deepEqual(await this.getAllSearchParams("button"), []) - } - - async "test submitter with blank formaction submits to the current page"() { - await this.clickSelector("#blank-formaction button") - await this.nextBody - - this.assert.ok(await this.hasSelector("#blank-formaction"), "overrides form[action] navigation") - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - } - - async "test input named action with no action attribute"() { - await this.clickSelector("#action-input form.no-action [type=submit]") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.equal(await this.getSearchParam("action"), "1") - this.assert.equal(await this.getSearchParam("query"), "1") - } - - async "test input named action with action attribute"() { - await this.clickSelector("#action-input form.action [type=submit]") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.getSearchParam("action"), "1") - this.assert.equal(await this.getSearchParam("query"), "1") - } - - async "test invalid form submission with unprocessable entity status"() { - await this.clickSelector("#reject form.unprocessable_entity input[type=submit]") - await this.nextBody + assert.notOk(await formSubmitStarted(page)) +}) - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "Unprocessable Entity", "renders the response HTML") - this.assert.notOk(await this.hasSelector("#frame form.reject"), "replaces entire page") - } +test("test frame form submission ignores submissions with their defaultPrevented", async ({ page }) => { + await page.evaluate(() => document.addEventListener("submit", (event) => event.preventDefault(), true)) + await page.click("#frame .redirect [type=submit]") + await nextBeat() - async "test invalid form submission with long form"() { - await this.scrollToSelector("#reject form.unprocessable_entity_with_tall_form input[type=submit]") - await this.clickSelector("#reject form.unprocessable_entity_with_tall_form input[type=submit]") - await this.nextBody + assert.equal(await page.textContent("#frame h2"), "Frame: Form") + assert.equal(await page.getAttribute("#frame", "src"), null, "does not navigate frame") +}) - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "Unprocessable Entity", "renders the response HTML") - this.assert(await this.isScrolledToTop(), "page is scrolled to the top") - this.assert.notOk(await this.hasSelector("#frame form.reject"), "replaces entire page") - } - - async "test invalid form submission with server error status"() { - this.assert(await this.hasSelector("head > #form-fixture-styles")) - await this.clickSelector("#reject form.internal_server_error input[type=submit]") - await this.nextBody - - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "Internal Server Error", "renders the response HTML") - this.assert.notOk(await this.hasSelector("head > #form-fixture-styles"), "replaces head") - this.assert.notOk(await this.hasSelector("#frame form.reject"), "replaces entire page") - } +test("test form submission with [data-turbo=false] on the form", async ({ page }) => { + await page.click('#turbo-false form[data-turbo="false"] input[type=submit]') + await waitUntilSelector(page, "#element-id") - async "test submitter form submission reads button attributes"() { - const button = await this.querySelector("#submitter form button[type=submit]") - await button.click() - await this.nextBody + assert.notOk(await formSubmitStarted(page)) +}) - this.assert.equal(await this.pathname, "/src/tests/fixtures/two.html") - this.assert.equal(await this.visitAction, "advance") - } +test("test form submission with [data-turbo=false] on the submitter", async ({ page }) => { + await page.click('#turbo-false form:not([data-turbo]) input[data-turbo="false"]') + await waitUntilSelector(page, "#element-id") - async "test submitter POST form submission with multipart/form-data formenctype"() { - await this.clickSelector("#submitter form[method=post]:not([enctype]) input[formenctype]") - await this.nextBeat + assert.notOk(await formSubmitStarted(page)) +}) - const enctype = await this.getSearchParam("enctype") - this.assert.ok(enctype?.startsWith("multipart/form-data"), "submits a multipart/form-data request") - } +test("test form submission skipped within method=dialog", async ({ page }) => { + await page.click('#dialog-method [type="submit"]') + await nextBeat() - async "test submitter GET submission from submitter with data-turbo-frame"() { - await this.clickSelector("#submitter form[method=get] [type=submit][data-turbo-frame]") - await this.nextBeat + assert.notOk(await formSubmitStarted(page)) +}) - const message = await this.querySelector("#frame div.message") - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "Form") - this.assert.equal(await message.getVisibleText(), "Frame redirected") - } +test("test form submission skipped with submitter formmethod=dialog", async ({ page }) => { + await page.click('#dialog-formmethod-turbo-frame [formmethod="dialog"]') + await nextBeat() - async "test submitter POST submission from submitter with data-turbo-frame"() { - await this.clickSelector("#submitter form[method=post] [type=submit][data-turbo-frame]") - await this.nextBeat + assert.notOk(await formSubmitEnded(page)) +}) - const message = await this.querySelector("#frame div.message") - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "Form") - this.assert.equal(await message.getVisibleText(), "Frame redirected") - } - - async "test frame form GET submission from submitter with data-turbo-frame=_top"() { - await this.clickSelector("#frame form[method=get] [type=submit][data-turbo-frame=_top]") - await this.nextBody - - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "One") - } - - async "test frame form POST submission from submitter with data-turbo-frame=_top"() { - await this.clickSelector("#frame form[method=post] [type=submit][data-turbo-frame=_top]") - await this.nextBody - - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "One") - } - - async "test frame POST form targetting frame submission"() { - await this.clickSelector("#targets-frame-post-form-submit") - - this.assert.ok(await this.formSubmitStarted, "fires turbo:submit-start") - - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") - - this.assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) - this.assert.equal("frame", fetchOptions.headers["Turbo-Frame"]) - - await this.nextEventNamed("turbo:before-fetch-response") - - this.assert.ok(await this.formSubmitEnded, "fires turbo:submit-end") - - await this.nextEventNamed("turbo:frame-render") - await this.nextEventNamed("turbo:frame-load") - - const otherEvents = await this.eventLogChannel.read() - this.assert.equal(otherEvents.length, 0, "no more events") - - const src = (await (await this.querySelector("#frame")).getAttribute("src")) || "" - this.assert.equal(new URL(src).pathname, "/src/tests/fixtures/frames/frame.html") - } - - async "test frame POST form targetting frame toggles submitter's [disabled] attribute"() { - await this.clickSelector("#targets-frame-post-form-submit") - - this.assert.equal( - await this.nextAttributeMutationNamed("targets-frame-post-form-submit", "disabled"), - "", - "sets [disabled] on the submitter" - ) - this.assert.equal( - await this.nextAttributeMutationNamed("targets-frame-post-form-submit", "disabled"), - null, - "removes [disabled] from the submitter" - ) - } - - async "test frame GET form targetting frame submission"() { - await this.clickSelector("#targets-frame-get-form-submit") - - this.assert.ok(await this.formSubmitStarted, "fires turbo:submit-start") - - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") - - this.assert.notOk(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) - this.assert.equal("frame", fetchOptions.headers["Turbo-Frame"]) - - await this.nextEventNamed("turbo:before-fetch-response") - - this.assert.ok(await this.formSubmitEnded, "fires turbo:submit-end") - - await this.nextEventNamed("turbo:frame-render") - await this.nextEventNamed("turbo:frame-load") - - const otherEvents = await this.eventLogChannel.read() - this.assert.equal(otherEvents.length, 0, "no more events") - - const src = (await (await this.querySelector("#frame")).getAttribute("src")) || "" - this.assert.equal(new URL(src).pathname, "/src/tests/fixtures/frames/frame.html") - } - - async "test frame GET form targetting frame toggles submitter's [disabled] attribute"() { - await this.clickSelector("#targets-frame-get-form-submit") - - this.assert.equal( - await this.nextAttributeMutationNamed("targets-frame-get-form-submit", "disabled"), - "", - "sets [disabled] on the submitter" - ) - this.assert.equal( - await this.nextAttributeMutationNamed("targets-frame-get-form-submit", "disabled"), - null, - "removes [disabled] from the submitter" - ) - } - - async "test frame form GET submission from submitter referencing another frame"() { - await this.clickSelector("#frame form[method=get] [type=submit][data-turbo-frame=hello]") - await this.nextBeat - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#hello h2") - this.assert.equal(await frameTitle.getVisibleText(), "Hello from a frame") - this.assert.equal(await title.getVisibleText(), "Form") - } - - async "test frame form POST submission from submitter referencing another frame"() { - await this.clickSelector("#frame form[method=post] [type=submit][data-turbo-frame=hello]") - await this.nextBeat - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#hello h2") - this.assert.equal(await frameTitle.getVisibleText(), "Hello from a frame") - this.assert.equal(await title.getVisibleText(), "Form") - } - - async "test frame form submission with redirect response"() { - const path = (await this.attributeForSelector("#frame form.redirect input[name=path]", "value")) || "" - const url = new URL(path, "http://localhost:9000") - url.searchParams.set("enctype", "application/x-www-form-urlencoded;charset=UTF-8") - - const button = await this.querySelector("#frame form.redirect input[type=submit]") - await button.click() - await this.nextBeat - - const message = await this.querySelector("#frame div.message") - this.assert.notOk(await this.hasSelector("#frame form.redirect")) - this.assert.equal(await message.getVisibleText(), "Frame redirected") - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html", "does not redirect _top") - this.assert.notOk(await this.search, "does not redirect _top") - this.assert.equal(await this.attributeForSelector("#frame", "src"), url.href, "redirects the target frame") - } - - async "test frame form submission toggles the ancestor frame's [aria-busy] attribute"() { - await this.clickSelector("#frame form.redirect input[type=submit]") - await this.nextBeat - - this.assert.equal(await this.nextAttributeMutationNamed("frame", "busy"), "", "sets [busy] on the #frame") - this.assert.equal( - await this.nextAttributeMutationNamed("frame", "aria-busy"), - "true", - "sets [aria-busy] on the #frame" - ) - this.assert.equal(await this.nextAttributeMutationNamed("frame", "busy"), null, "removes [busy] from the #frame") - this.assert.equal( - await this.nextAttributeMutationNamed("frame", "aria-busy"), - null, - "removes [aria-busy] from the #frame" - ) - } - - async "test frame form submission toggles the target frame's [aria-busy] attribute"() { - await this.clickSelector('#targets-frame form.frame [type="submit"]') - await this.nextBeat - - this.assert.equal(await this.nextAttributeMutationNamed("frame", "busy"), "", "sets [busy] on the #frame") - this.assert.equal( - await this.nextAttributeMutationNamed("frame", "aria-busy"), - "true", - "sets [aria-busy] on the #frame" - ) - - const title = await this.querySelector("#frame h2") - this.assert.equal(await title.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.nextAttributeMutationNamed("frame", "busy"), null, "removes [busy] from the #frame") - this.assert.equal( - await this.nextAttributeMutationNamed("frame", "aria-busy"), - null, - "removes [aria-busy] from the #frame" - ) - } - - async "test frame form submission with empty created response"() { - const htmlBefore = await this.outerHTMLForSelector("#frame") - const button = await this.querySelector("#frame form.created input[type=submit]") - await button.click() - await this.nextBeat - - const htmlAfter = await this.outerHTMLForSelector("#frame") - this.assert.equal(htmlAfter, htmlBefore) - } - - async "test frame form submission with empty no-content response"() { - const htmlBefore = await this.outerHTMLForSelector("#frame") - const button = await this.querySelector("#frame form.no-content input[type=submit]") - await button.click() - await this.nextBeat - - const htmlAfter = await this.outerHTMLForSelector("#frame") - this.assert.equal(htmlAfter, htmlBefore) - } - - async "test frame form submission within a frame submits the Turbo-Frame header"() { - await this.clickSelector("#frame form.redirect input[type=submit]") - - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") - - this.assert.ok(fetchOptions.headers["Turbo-Frame"], "submits with the Turbo-Frame header") - } - - async "test invalid frame form submission with unprocessable entity status"() { - await this.clickSelector("#frame form.unprocessable_entity input[type=submit]") - - this.assert.ok(await this.formSubmitStarted, "fires turbo:submit-start") - await this.nextEventNamed("turbo:before-fetch-request") - await this.nextEventNamed("turbo:before-fetch-response") - this.assert.ok(await this.formSubmitEnded, "fires turbo:submit-end") - await this.nextEventNamed("turbo:frame-render") - await this.nextEventNamed("turbo:frame-load") - - const otherEvents = await this.eventLogChannel.read() - this.assert.equal(otherEvents.length, 0, "no more events") - - const title = await this.querySelector("#frame h2") - this.assert.ok(await this.hasSelector("#reject form"), "only replaces frame") - this.assert.equal(await title.getVisibleText(), "Frame: Unprocessable Entity") - } - - async "test invalid frame form submission with internal server errror status"() { - await this.clickSelector("#frame form.internal_server_error input[type=submit]") - - this.assert.ok(await this.formSubmitStarted, "fires turbo:submit-start") - await this.nextEventNamed("turbo:before-fetch-request") - await this.nextEventNamed("turbo:before-fetch-response") - this.assert.ok(await this.formSubmitEnded, "fires turbo:submit-end") - await this.nextEventNamed("turbo:frame-render") - await this.nextEventNamed("turbo:frame-load") - - const otherEvents = await this.eventLogChannel.read() - this.assert.equal(otherEvents.length, 0, "no more events") - - const title = await this.querySelector("#frame h2") - this.assert.ok(await this.hasSelector("#reject form"), "only replaces frame") - this.assert.equal(await title.getVisibleText(), "Frame: Internal Server Error") - } - - async "test frame form submission with stream response"() { - const button = await this.querySelector("#frame form.stream input[type=submit]") - await button.click() - await this.nextBeat - - const message = await this.querySelector("#frame div.message") - this.assert.ok(await this.hasSelector("#frame form.redirect")) - this.assert.equal(await message.getVisibleText(), "Hello!") - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - this.assert.notOk(await this.propertyForSelector("#frame", "src"), "does not change frame's src") - } - - async "test frame form submission with HTTP verb other than GET or POST"() { - await this.clickSelector("#frame form.put.stream input[type=submit]") - await this.nextBeat - - const message = await this.querySelector("#frame div.message") - this.assert.ok(await this.hasSelector("#frame form.redirect")) - this.assert.equal(await message.getVisibleText(), "1: Hello!") - this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html") - } - - async "test frame form submission with [data-turbo=false] on the form"() { - await this.clickSelector('#frame form[data-turbo="false"] input[type=submit]') - await this.nextBody - await this.querySelector("#element-id") - - this.assert.notOk(await this.formSubmitStarted) - } - - async "test frame form submission with [data-turbo=false] on the submitter"() { - await this.clickSelector('#frame form:not([data-turbo]) input[data-turbo="false"]') - await this.nextBody - await this.querySelector("#element-id") - - this.assert.notOk(await this.formSubmitStarted) - } - - async "test frame form submission ignores submissions with their defaultPrevented"() { - await this.evaluate(() => document.addEventListener("submit", (event) => event.preventDefault(), true)) - await this.clickSelector("#frame .redirect [type=submit]") - await this.nextBeat - - this.assert.equal(await (await this.querySelector("#frame h2")).getVisibleText(), "Frame: Form") - this.assert.equal(await this.attributeForSelector("#frame", "src"), null, "does not navigate frame") - } +test("test form submission targetting frame skipped within method=dialog", async ({ page }) => { + await page.click("#dialog-method-turbo-frame button") + await nextBeat() - async "test form submission with [data-turbo=false] on the form"() { - await this.clickSelector('#turbo-false form[data-turbo="false"] input[type=submit]') - await this.nextBody - await this.querySelector("#element-id") + assert.notOk(await formSubmitEnded(page)) +}) - this.assert.notOk(await this.formSubmitStarted) - } +test("test form submission targetting frame skipped with submitter formmethod=dialog", async ({ page }) => { + await page.click('#dialog-formmethod [formmethod="dialog"]') + await nextBeat() - async "test form submission with [data-turbo=false] on the submitter"() { - await this.clickSelector('#turbo-false form:not([data-turbo]) input[data-turbo="false"]') - await this.nextBody - await this.querySelector("#element-id") + assert.notOk(await formSubmitStarted(page)) +}) - this.assert.notOk(await this.formSubmitStarted) - } +test("test form submission targets disabled frame", async ({ page }) => { + await page.evaluate(() => document.getElementById("frame")?.setAttribute("disabled", "")) + await page.click('#targets-frame form.one [type="submit"]') + await nextBody(page) - async "test form submission skipped within method=dialog"() { - await this.clickSelector('#dialog-method [type="submit"]') - await this.nextBeat + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") +}) - this.assert.notOk(await this.formSubmitStarted) - } +test("test form submission targeting a frame submits the Turbo-Frame header", async ({ page }) => { + await page.click('#targets-frame [type="submit"]') - async "test form submission skipped with submitter formmethod=dialog"() { - await this.clickSelector('#dialog-formmethod-turbo-frame [formmethod="dialog"]') - await this.nextBeat + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") - this.assert.notOk(await this.formSubmitEnded) - } + assert.ok(fetchOptions.headers["Turbo-Frame"], "submits with the Turbo-Frame header") +}) - async "test form submission targetting frame skipped within method=dialog"() { - await this.clickSelector("#dialog-method-turbo-frame button") - await this.nextBeat +test("test link method form submission inside frame", async ({ page }) => { + await page.click("#link-method-inside-frame") + await nextBeat() - this.assert.notOk(await this.formSubmitEnded) - } + assert.equal(await await page.textContent("#frame h2"), "Frame: Loaded") + assert.notOk(await hasSelector(page, "#nested-child")) +}) - async "test form submission targetting frame skipped with submitter formmethod=dialog"() { - await this.clickSelector('#dialog-formmethod [formmethod="dialog"]') - await this.nextBeat +test("test link method form submission inside frame with data-turbo-frame=_top", async ({ page }) => { + await page.click("#link-method-inside-frame-target-top") + await nextBody(page) - this.assert.notOk(await this.formSubmitStarted) - } + const title = await page.locator("h1") + assert.equal(await title.textContent(), "Hello") +}) - async "test form submission targets disabled frame"() { - await this.remote.execute(() => document.getElementById("frame")?.setAttribute("disabled", "")) - await this.clickSelector('#targets-frame form.one [type="submit"]') - await this.nextBody +test("test link method form submission inside frame with data-turbo-frame target", async ({ page }) => { + await page.click("#link-method-inside-frame-with-target") + await nextBeat() - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - } + const title = await page.locator("h1") + const frameTitle = await page.locator("#hello h2") + assert.equal(await frameTitle.textContent(), "Hello from a frame") + assert.equal(await title.textContent(), "Form") +}) - async "test form submission targeting a frame submits the Turbo-Frame header"() { - await this.clickSelector('#targets-frame [type="submit"]') +test("test stream link method form submission inside frame", async ({ page }) => { + await page.click("#stream-link-method-inside-frame") + await nextBeat() - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") + const message = page.locator("#frame div.message") + assert.equal(await message.textContent(), "Link!") +}) - this.assert.ok(fetchOptions.headers["Turbo-Frame"], "submits with the Turbo-Frame header") - } +test("test stream link GET method form submission inside frame", async ({ page }) => { + await page.click("#stream-link-get-method-inside-frame") - async "test link method form submission inside frame"() { - await this.clickSelector("#link-method-inside-frame") - await this.nextBeat + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") - const title = await this.querySelector("#frame h2") - this.assert.equal(await title.getVisibleText(), "Frame: Loaded") - this.assert.notOk(await this.hasSelector("#nested-child")) - } + assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) +}) - async "test link method form submission inside frame with data-turbo-frame=_top"() { - await this.clickSelector("#link-method-inside-frame-target-top") - await this.nextBody +test("test stream link inside frame", async ({ page }) => { + await page.click("#stream-link-inside-frame") - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "Hello") - } + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") - async "test link method form submission inside frame with data-turbo-frame target"() { - await this.clickSelector("#link-method-inside-frame-with-target") - await this.nextBeat + assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) +}) - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#hello h2") - this.assert.equal(await frameTitle.getVisibleText(), "Hello from a frame") - this.assert.equal(await title.getVisibleText(), "Form") - } +test("test stream link outside frame", async ({ page }) => { + await page.click("#stream-link-outside-frame") - async "test stream link method form submission inside frame"() { - await this.clickSelector("#stream-link-method-inside-frame") - await this.nextBeat + const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") - const message = await this.querySelector("#frame div.message") - this.assert.equal(await message.getVisibleText(), "Link!") - } + assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) +}) - async "test stream link GET method form submission inside frame"() { - await this.clickSelector("#stream-link-get-method-inside-frame") +test("test link method form submission within form inside frame", async ({ page }) => { + await page.click("#stream-link-method-within-form-inside-frame") + await nextBeat() - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") + const message = page.locator("#frame div.message") + assert.equal(await message.textContent(), "Link!") +}) - this.assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) - } +test("test link method form submission inside frame with confirmation confirmed", async ({ page }) => { + page.on("dialog", (dialog) => { + assert.equal(dialog.message(), "Are you sure?") + dialog.accept() + }) - async "test stream link inside frame"() { - await this.clickSelector("#stream-link-inside-frame") + await page.click("#link-method-inside-frame-with-confirmation") + await nextBeat() - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") + const message = page.locator("#frame div.message") + assert.equal(await message.textContent(), "Link!") +}) - this.assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) - } +test("test link method form submission inside frame with confirmation cancelled", async ({ page }) => { + page.on("dialog", (dialog) => { + assert.equal(dialog.message(), "Are you sure?") + dialog.dismiss() + }) - async "test stream link outside frame"() { - await this.clickSelector("#stream-link-outside-frame") + await page.click("#link-method-inside-frame-with-confirmation") + await nextBeat() - const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") + assert.notOk(await hasSelector(page, "#frame div.message"), "Not confirming form submission does not submit the form") +}) - this.assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) - } +test("test link method form submission outside frame", async ({ page }) => { + await page.click("#link-method-outside-frame") + await nextBody(page) - async "test link method form submission within form inside frame"() { - await this.clickSelector("#stream-link-method-within-form-inside-frame") - await this.nextBeat + const title = await page.locator("h1") + assert.equal(await title.textContent(), "Hello") +}) - const message = await this.querySelector("#frame div.message") - this.assert.equal(await message.getVisibleText(), "Link!") - } +test("test stream link method form submission outside frame", async ({ page }) => { + await page.click("#stream-link-method-outside-frame") + await nextBeat() - async "test link method form submission inside frame with confirmation confirmed"() { - await this.clickSelector("#link-method-inside-frame-with-confirmation") + const message = page.locator("#frame div.message") + assert.equal(await message.textContent(), "Link!") +}) - this.assert.equal(await this.getAlertText(), "Are you sure?") - await this.acceptAlert() +test("test link method form submission within form outside frame", async ({ page }) => { + await page.click("#link-method-within-form-outside-frame") + await nextBody(page) - await this.nextBeat + const title = await page.locator("h1") + assert.equal(await title.textContent(), "Hello") +}) - const message = await this.querySelector("#frame div.message") - this.assert.equal(await message.getVisibleText(), "Link!") - } +test("test stream link method form submission within form outside frame", async ({ page }) => { + await page.click("#stream-link-method-within-form-outside-frame") + await nextBeat() - async "test link method form submission inside frame with confirmation cancelled"() { - await this.clickSelector("#link-method-inside-frame-with-confirmation") + assert.equal(await page.textContent("#frame div.message"), "Link!") +}) - this.assert.equal(await this.getAlertText(), "Are you sure?") - await this.dismissAlert() +test("test form submission with form mode off", async ({ page }) => { + await page.evaluate(() => window.Turbo.setFormMode("off")) + await page.click("#standard form.turbo-enabled input[type=submit]") - await this.nextBeat + assert.notOk(await formSubmitStarted(page)) +}) - this.assert.notOk( - await this.hasSelector("#frame div.message"), - "Not confirming form submission does not submit the form" - ) - } +test("test form submission with form mode optin and form not enabled", async ({ page }) => { + await page.evaluate(() => window.Turbo.setFormMode("optin")) + await page.click("#standard form.redirect input[type=submit]") - async "test link method form submission outside frame"() { - await this.clickSelector("#link-method-outside-frame") - await this.nextBody + assert.notOk(await formSubmitStarted(page)) +}) - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "Hello") - } +test("test form submission with form mode optin and form enabled", async ({ page }) => { + await page.evaluate(() => window.Turbo.setFormMode("optin")) + await page.click("#standard form.turbo-enabled input[type=submit]") - async "test stream link method form submission outside frame"() { - await this.clickSelector("#stream-link-method-outside-frame") - await this.nextBeat + assert.ok(await formSubmitStarted(page)) +}) - const message = await this.querySelector("#frame div.message") - this.assert.equal(await message.getVisibleText(), "Link!") - } +test("test turbo:before-fetch-request fires on the form element", async ({ page }) => { + await page.click('#targets-frame form.one [type="submit"]') + assert.ok(await nextEventOnTarget(page, "form_one", "turbo:before-fetch-request")) +}) - async "test link method form submission within form outside frame"() { - await this.clickSelector("#link-method-within-form-outside-frame") - await this.nextBody +test("test turbo:before-fetch-response fires on the form element", async ({ page }) => { + await page.click('#targets-frame form.one [type="submit"]') + assert.ok(await nextEventOnTarget(page, "form_one", "turbo:before-fetch-response")) +}) - const title = await this.querySelector("h1") - this.assert.equal(await title.getVisibleText(), "Hello") - } +test("test POST to external action ignored", async ({ page }) => { + await page.click("#submit-external") + await noNextEventNamed(page, "turbo:before-fetch-request") + await nextBody(page) - async "test stream link method form submission within form outside frame"() { - await this.clickSelector("#stream-link-method-within-form-outside-frame") - await this.nextBeat + assert.equal(page.url(), "https://httpbin.org/post") +}) - const message = await this.querySelector("#frame div.message") - this.assert.equal(await message.getVisibleText(), "Link!") - } +test("test POST to external action within frame ignored", async ({ page }) => { + await page.click("#submit-external-within-ignored") + await noNextEventNamed(page, "turbo:before-fetch-request") + await nextBody(page) - async "test form submission with form mode off"() { - await this.remote.execute(() => window.Turbo.setFormMode("off")) - await this.clickSelector("#standard form.turbo-enabled input[type=submit]") + assert.equal(page.url(), "https://httpbin.org/post") +}) - this.assert.notOk(await this.formSubmitStarted) - } +test("test POST to external action targetting frame ignored", async ({ page }) => { + await page.click("#submit-external-target-ignored") + await noNextEventNamed(page, "turbo:before-fetch-request") + await nextBody(page) - async "test form submission with form mode optin and form not enabled"() { - await this.remote.execute(() => window.Turbo.setFormMode("optin")) - await this.clickSelector("#standard form.redirect input[type=submit]") + assert.equal(page.url(), "https://httpbin.org/post") +}) - this.assert.notOk(await this.formSubmitStarted) - } - - async "test form submission with form mode optin and form enabled"() { - await this.remote.execute(() => window.Turbo.setFormMode("optin")) - await this.clickSelector("#standard form.turbo-enabled input[type=submit]") - - this.assert.ok(await this.formSubmitStarted) - } - - async "test turbo:before-fetch-request fires on the form element"() { - await this.clickSelector('#targets-frame form.one [type="submit"]') - this.assert.ok(await this.nextEventOnTarget("form_one", "turbo:before-fetch-request")) - } - - async "test turbo:before-fetch-response fires on the form element"() { - await this.clickSelector('#targets-frame form.one [type="submit"]') - this.assert.ok(await this.nextEventOnTarget("form_one", "turbo:before-fetch-response")) - } - - async "test POST to external action ignored"() { - await this.clickSelector("#submit-external") - await this.noNextEventNamed("turbo:before-fetch-request") - await this.nextBody - - this.assert.equal(await this.location, "https://httpbin.org/post") - } - - async "test POST to external action within frame ignored"() { - await this.clickSelector("#submit-external-within-ignored") - await this.noNextEventNamed("turbo:before-fetch-request") - await this.nextBody - - this.assert.equal(await this.location, "https://httpbin.org/post") - } - - async "test POST to external action targetting frame ignored"() { - await this.clickSelector("#submit-external-target-ignored") - await this.noNextEventNamed("turbo:before-fetch-request") - await this.nextBody - - this.assert.equal(await this.location, "https://httpbin.org/post") - } - - get formSubmitStarted() { - return this.getFromLocalStorage("formSubmitStarted") - } - - get formSubmitEnded() { - return this.getFromLocalStorage("formSubmitEnded") - } +function formSubmitStarted(page: Page) { + return getFromLocalStorage(page, "formSubmitStarted") } -FormSubmissionTests.registerSuite() +function formSubmitEnded(page: Page) { + return getFromLocalStorage(page, "formSubmitEnded") +} diff --git a/src/tests/functional/frame_navigation_tests.ts b/src/tests/functional/frame_navigation_tests.ts index 513edb5bb..212b709c5 100644 --- a/src/tests/functional/frame_navigation_tests.ts +++ b/src/tests/functional/frame_navigation_tests.ts @@ -1,27 +1,24 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { nextEventOnTarget } from "../helpers/page" -export class FrameNavigationTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/frame_navigation.html") - } +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/frame_navigation.html") +}) - async "test frame navigation with descendant link"() { - await this.clickSelector("#inside") +test("test frame navigation with descendant link", async ({ page }) => { + await page.click("#inside") - await this.nextEventOnTarget("frame", "turbo:frame-load") - } + await nextEventOnTarget(page, "frame", "turbo:frame-load") +}) - async "test frame navigation with self link"() { - await this.clickSelector("#self") +test("test frame navigation with self link", async ({ page }) => { + await page.click("#self") - await this.nextEventOnTarget("frame", "turbo:frame-load") - } + await nextEventOnTarget(page, "frame", "turbo:frame-load") +}) - async "test frame navigation with exterior link"() { - await this.clickSelector("#outside") +test("test frame navigation with exterior link", async ({ page }) => { + await page.click("#outside") - await this.nextEventOnTarget("frame", "turbo:frame-load") - } -} - -FrameNavigationTests.registerSuite() + await nextEventOnTarget(page, "frame", "turbo:frame-load") +}) diff --git a/src/tests/functional/frame_tests.ts b/src/tests/functional/frame_tests.ts index a9afd1316..0f0ec4045 100644 --- a/src/tests/functional/frame_tests.ts +++ b/src/tests/functional/frame_tests.ts @@ -1,683 +1,703 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" - -export class FrameTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/frames.html") - } - - async "test navigating a frame a second time does not leak event listeners"() { - await this.withoutChangingEventListenersCount(async () => { - await this.clickSelector("#outer-frame-link") - await this.nextEventOnTarget("frame", "turbo:frame-load") - await this.clickSelector("#outside-frame-form") - await this.nextEventOnTarget("frame", "turbo:frame-load") - await this.clickSelector("#outer-frame-link") - await this.nextEventOnTarget("frame", "turbo:frame-load") - }) - } - - async "test following a link preserves the current element's attributes"() { - const currentPath = await this.pathname - - await this.clickSelector("#hello a") - await this.nextBeat - - const frame = await this.querySelector("turbo-frame#frame") - this.assert.equal(await frame.getAttribute("data-loaded-from"), currentPath) - this.assert.equal(await frame.getAttribute("src"), await this.propertyForSelector("#hello a", "href")) - } - - async "test following a link sets the frame element's [src]"() { - await this.clickSelector("#link-frame-with-search-params") - - const { url } = await this.nextEventOnTarget("frame", "turbo:before-fetch-request") - const fetchRequestUrl = new URL(url) - - this.assert.equal(fetchRequestUrl.pathname, "/src/tests/fixtures/frames/frame.html") - this.assert.equal(fetchRequestUrl.searchParams.get("key"), "value", "fetch request encodes query parameters") - - await this.nextBeat - const src = new URL((await this.attributeForSelector("#frame", "src")) || "") - - this.assert.equal(src.pathname, "/src/tests/fixtures/frames/frame.html") - this.assert.equal(src.searchParams.get("key"), "value", "[src] attribute encodes query parameters") - } - - async "test a frame whose src references itself does not infinitely loop"() { - await this.clickSelector("#frame-self") - - await this.nextEventOnTarget("frame", "turbo:frame-render") - await this.nextEventOnTarget("frame", "turbo:frame-load") - - const otherEvents = await this.eventLogChannel.read() - this.assert.equal(otherEvents.length, 0, "no more events") - } - - async "test following a link driving a frame toggles the [aria-busy=true] attribute"() { - await this.clickSelector("#hello a") - - this.assert.equal(await this.nextAttributeMutationNamed("frame", "busy"), "", "sets [busy] on the #frame") - this.assert.equal( - await this.nextAttributeMutationNamed("frame", "aria-busy"), - "true", - "sets [aria-busy=true] on the #frame" - ) - this.assert.equal(await this.nextAttributeMutationNamed("frame", "busy"), null, "removes [busy] on the #frame") - this.assert.equal( - await this.nextAttributeMutationNamed("frame", "aria-busy"), - null, - "removes [aria-busy] from the #frame" - ) - } - - async "test following a link to a page without a matching frame results in an empty frame"() { - await this.clickSelector("#missing a") - await this.nextBeat - this.assert.notOk(await this.innerHTMLForSelector("#missing")) - } - - async "test following a link within a frame with a target set navigates the target frame"() { - await this.clickSelector("#hello a") - await this.nextBeat - - const frameText = await this.querySelector("#frame h2") - this.assert.equal(await frameText.getVisibleText(), "Frame: Loaded") - } - - async "test following a link in rapid succession cancels the previous request"() { - await this.clickSelector("#outside-frame-form") - await this.clickSelector("#outer-frame-link") - await this.nextBeat - - const frameText = await this.querySelector("#frame h2") - this.assert.equal(await frameText.getVisibleText(), "Frame: Loaded") - } - - async "test following a link within a descendant frame whose ancestor declares a target set navigates the descendant frame"() { - const link = await this.querySelector("#nested-root[target=frame] #nested-child a:not([data-turbo-frame])") - const href = await link.getProperty("href") - - await link.click() - await this.nextBeat - - const frame = await this.querySelector("#frame h2") - const nestedRoot = await this.querySelector("#nested-root h2") - const nestedChild = await this.querySelector("#nested-child") - this.assert.equal(await frame.getVisibleText(), "Frames: #frame") - this.assert.equal(await nestedRoot.getVisibleText(), "Frames: #nested-root") - this.assert.equal(await nestedChild.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.attributeForSelector("#frame", "src"), null) - this.assert.equal(await this.attributeForSelector("#nested-root", "src"), null) - this.assert.equal(await this.attributeForSelector("#nested-child", "src"), href) - } - - async "test following a link that declares data-turbo-frame within a frame whose ancestor respects the override"() { - await this.clickSelector("#nested-root[target=frame] #nested-child a[data-turbo-frame]") - await this.nextBeat - - const frameText = await this.querySelector("body > h1") - this.assert.equal(await frameText.getVisibleText(), "One") - this.assert.notOk(await this.hasSelector("#frame")) - this.assert.notOk(await this.hasSelector("#nested-root")) - this.assert.notOk(await this.hasSelector("#nested-child")) - } - - async "test following a form within a nested frame with form target top"() { - await this.clickSelector("#nested-child-navigate-form-top-submit") - await this.nextBeat - - const frameText = await this.querySelector("body > h1") - this.assert.equal(await frameText.getVisibleText(), "One") - this.assert.notOk(await this.hasSelector("#frame")) - this.assert.notOk(await this.hasSelector("#nested-root")) - this.assert.notOk(await this.hasSelector("#nested-child")) - } - - async "test following a form within a nested frame with child frame target top"() { - await this.clickSelector("#nested-child-navigate-top-submit") - await this.nextBeat - - const frameText = await this.querySelector("body > h1") - this.assert.equal(await frameText.getVisibleText(), "One") - this.assert.notOk(await this.hasSelector("#frame")) - this.assert.notOk(await this.hasSelector("#nested-root")) - this.assert.notOk(await this.hasSelector("#nested-child-navigate-top")) - } - - async "test following a link within a frame with target=_top navigates the page"() { - this.assert.equal(await this.attributeForSelector("#navigate-top", "src"), null) - - await this.clickSelector("#navigate-top a:not([data-turbo-frame])") - await this.nextBeat - - const frameText = await this.querySelector("body > h1") - this.assert.equal(await frameText.getVisibleText(), "One") - this.assert.notOk(await this.hasSelector("#navigate-top a")) - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.getSearchParam("key"), "value") - } - - async "test following a link that declares data-turbo-frame='_self' within a frame with target=_top navigates the frame itself"() { - this.assert.equal(await this.attributeForSelector("#navigate-top", "src"), null) - - await this.clickSelector("#navigate-top a[data-turbo-frame='_self']") - await this.nextBeat - - const title = await this.querySelector("body > h1") - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.ok(await this.hasSelector("#navigate-top")) - const frame = await this.querySelector("#navigate-top") - this.assert.equal(await frame.getVisibleText(), "Replaced only the frame") - } - - async "test following a link to a page with a which lazily loads a matching frame"() { - await this.nextBeat - await this.clickSelector("#recursive summary") - this.assert.ok(await this.querySelector("#recursive details[open]")) - - await this.clickSelector("#recursive a") - await this.nextBeat - this.assert.ok(await this.querySelector("#recursive details:not([open])")) - } - - async "test submitting a form that redirects to a page with a which lazily loads a matching frame"() { - await this.nextBeat - await this.clickSelector("#recursive summary") - this.assert.ok(await this.querySelector("#recursive details[open]")) - - await this.clickSelector("#recursive input[type=submit]") - await this.nextBeat - this.assert.ok(await this.querySelector("#recursive details:not([open])")) - } - - async "test removing [disabled] attribute from eager-loaded frame navigates it"() { - await this.remote.execute(() => document.getElementById("frame")?.setAttribute("disabled", "")) - await this.remote.execute(() => - document.getElementById("frame")?.setAttribute("src", "/src/tests/fixtures/frames/frame.html") - ) - - this.assert.ok( - await this.noNextEventNamed("turbo:before-fetch-request"), - "[disabled] frames do not submit requests" - ) - - await this.remote.execute(() => document.getElementById("frame")?.removeAttribute("disabled")) - - await this.nextEventNamed("turbo:before-fetch-request") - } - - async "test evaluates frame script elements on each render"() { - this.assert.equal(await this.frameScriptEvaluationCount, undefined) - - this.clickSelector("#body-script-link") - await this.sleep(200) - this.assert.equal(await this.frameScriptEvaluationCount, 1) - - this.clickSelector("#body-script-link") - await this.sleep(200) - this.assert.equal(await this.frameScriptEvaluationCount, 2) - } - - async "test does not evaluate data-turbo-eval=false scripts"() { - this.clickSelector("#eval-false-script-link") - await this.nextBeat - this.assert.equal(await this.frameScriptEvaluationCount, undefined) - } - - async "test redirecting in a form is still navigatable after redirect"() { - await this.nextBeat - await this.clickSelector("#navigate-form-redirect") - await this.nextBeat - this.assert.ok(await this.querySelector("#form-redirect")) - - await this.nextBeat - await this.clickSelector("#submit-form") - await this.nextBeat - this.assert.ok(await this.querySelector("#form-redirected-header")) - - await this.nextBeat - await this.clickSelector("#navigate-form-redirect") - await this.nextBeat - this.assert.ok(await this.querySelector("#form-redirect-header")) - } - - async "test 'turbo:frame-render' is triggered after frame has finished rendering"() { - await this.clickSelector("#frame-part") - - await this.nextEventNamed("turbo:frame-render") // recursive - const { fetchResponse } = await this.nextEventNamed("turbo:frame-render") - - this.assert.include(fetchResponse.response.url, "/src/tests/fixtures/frames/part.html") - } - - async "test navigating a frame fires events"() { - await this.clickSelector("#outside-frame-form") - - const { fetchResponse } = await this.nextEventOnTarget("frame", "turbo:frame-render") - this.assert.include(fetchResponse.response.url, "/src/tests/fixtures/frames/form.html") - - await this.nextEventOnTarget("frame", "turbo:frame-load") - - const otherEvents = await this.eventLogChannel.read() - this.assert.equal(otherEvents.length, 0, "no more events") - } - - async "test following inner link reloads frame on every click"() { - await this.clickSelector("#hello a") - await this.nextEventNamed("turbo:before-fetch-request") - - await this.clickSelector("#hello a") - await this.nextEventNamed("turbo:before-fetch-request") - } - - async "test following outer link reloads frame on every click"() { - await this.clickSelector("#outer-frame-link") - await this.nextEventNamed("turbo:before-fetch-request") - - await this.clickSelector("#outer-frame-link") - await this.nextEventNamed("turbo:before-fetch-request") - } - - async "test following outer form reloads frame on every submit"() { - await this.clickSelector("#outer-frame-submit") - await this.nextEventNamed("turbo:before-fetch-request") +import { Page, test } from "@playwright/test" +import { assert, Assertion } from "chai" +import { + attributeForSelector, + hasSelector, + innerHTMLForSelector, + nextAttributeMutationNamed, + nextBeat, + nextEventNamed, + nextEventOnTarget, + noNextEventNamed, + noNextEventOnTarget, + pathname, + propertyForSelector, + readEventLogs, + scrollPosition, + scrollToSelector, + searchParams, +} from "../helpers/page" + +assert.equal = function (actual: any, expected: any, message?: string) { + actual = typeof actual == "string" ? actual.trim() : actual + expected = typeof expected == "string" ? expected.trim() : expected + + const assertExpectation = new Assertion(expected) + + assertExpectation.to.equal(expected, message) +} - await this.clickSelector("#outer-frame-submit") - await this.nextEventNamed("turbo:before-fetch-request") - } +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/frames.html") + await readEventLogs(page) +}) + +test("test navigating a frame a second time does not leak event listeners", async ({ page }) => { + await withoutChangingEventListenersCount(page, async () => { + await page.click("#outer-frame-link") + await nextEventOnTarget(page, "frame", "turbo:frame-load") + await page.click("#outside-frame-form") + await nextEventOnTarget(page, "frame", "turbo:frame-load") + await page.click("#outer-frame-link") + await nextEventOnTarget(page, "frame", "turbo:frame-load") + }) +}) + +test("test following a link preserves the current element's attributes", async ({ page }) => { + const currentPath = pathname(page.url()) + + await page.click("#hello a") + await nextBeat() + + const frame = await page.locator("turbo-frame#frame") + assert.equal(await frame.getAttribute("data-loaded-from"), currentPath) + assert.equal(await frame.getAttribute("src"), await propertyForSelector(page, "#hello a", "href")) +}) + +test("test following a link sets the frame element's [src]", async ({ page }) => { + await page.click("#link-frame-with-search-params") + + const { url } = await nextEventOnTarget(page, "frame", "turbo:before-fetch-request") + const fetchRequestUrl = new URL(url) + + assert.equal(fetchRequestUrl.pathname, "/src/tests/fixtures/frames/frame.html") + assert.equal(fetchRequestUrl.searchParams.get("key"), "value", "fetch request encodes query parameters") + + await nextBeat() + const src = new URL((await attributeForSelector(page, "#frame", "src")) || "") + + assert.equal(src.pathname, "/src/tests/fixtures/frames/frame.html") + assert.equal(src.searchParams.get("key"), "value", "[src] attribute encodes query parameters") +}) + +test("test a frame whose src references itself does not infinitely loop", async ({ page }) => { + await page.click("#frame-self") + + await nextEventOnTarget(page, "frame", "turbo:frame-render") + await nextEventOnTarget(page, "frame", "turbo:frame-load") + + const otherEvents = await readEventLogs(page) + assert.equal(otherEvents.length, 0, "no more events") +}) + +test("test following a link driving a frame toggles the [aria-busy=true] attribute", async ({ page }) => { + await page.click("#hello a") + + assert.equal(await nextAttributeMutationNamed(page, "frame", "busy"), "", "sets [busy] on the #frame") + assert.equal( + await nextAttributeMutationNamed(page, "frame", "aria-busy"), + "true", + "sets [aria-busy=true] on the #frame" + ) + assert.equal(await nextAttributeMutationNamed(page, "frame", "busy"), null, "removes [busy] on the #frame") + assert.equal( + await nextAttributeMutationNamed(page, "frame", "aria-busy"), + null, + "removes [aria-busy] from the #frame" + ) +}) + +test("test following a link to a page without a matching frame results in an empty frame", async ({ page }) => { + await page.click("#missing a") + await nextBeat() + assert.notOk(await innerHTMLForSelector(page, "#missing")) +}) + +test("test following a link within a frame with a target set navigates the target frame", async ({ page }) => { + await page.click("#hello a") + await nextBeat() + + const frameText = await page.textContent("#frame h2") + assert.equal(frameText, "Frame: Loaded") +}) + +test("test following a link in rapid succession cancels the previous request", async ({ page }) => { + await page.click("#outside-frame-form") + await page.click("#outer-frame-link") + await nextBeat() + + const frameText = await page.textContent("#frame h2") + assert.equal(frameText, "Frame: Loaded") +}) + +test("test following a link within a descendant frame whose ancestor declares a target set navigates the descendant frame", async ({ + page, +}) => { + const selector = "#nested-root[target=frame] #nested-child a:not([data-turbo-frame])" + const link = await page.locator(selector) + const href = await propertyForSelector(page, selector, "href") + + await link.click() + await nextBeat() + + const frame = await page.textContent("#frame h2") + const nestedRoot = await page.textContent("#nested-root h2") + const nestedChild = await page.textContent("#nested-child") + assert.equal(frame, "Frames: #frame") + assert.equal(nestedRoot, "Frames: #nested-root") + assert.equal(nestedChild, "Frame: Loaded") + assert.equal(await attributeForSelector(page, "#frame", "src"), null) + assert.equal(await attributeForSelector(page, "#nested-root", "src"), null) + assert.equal(await attributeForSelector(page, "#nested-child", "src"), href || "") +}) + +test("test following a link that declares data-turbo-frame within a frame whose ancestor respects the override", async ({ + page, +}) => { + await page.click("#nested-root[target=frame] #nested-child a[data-turbo-frame]") + await nextBeat() + + const frameText = await page.textContent("body > h1") + assert.equal(frameText, "One") + assert.notOk(await hasSelector(page, "#frame")) + assert.notOk(await hasSelector(page, "#nested-root")) + assert.notOk(await hasSelector(page, "#nested-child")) +}) + +test("test following a form within a nested frame with form target top", async ({ page }) => { + await page.click("#nested-child-navigate-form-top-submit") + await nextBeat() + + const frameText = await page.textContent("body > h1") + assert.equal(frameText, "One") + assert.notOk(await hasSelector(page, "#frame")) + assert.notOk(await hasSelector(page, "#nested-root")) + assert.notOk(await hasSelector(page, "#nested-child")) +}) + +test("test following a form within a nested frame with child frame target top", async ({ page }) => { + await page.click("#nested-child-navigate-top-submit") + await nextBeat() + + const frameText = await page.textContent("body > h1") + assert.equal(frameText, "One") + assert.notOk(await hasSelector(page, "#frame")) + assert.notOk(await hasSelector(page, "#nested-root")) + assert.notOk(await hasSelector(page, "#nested-child-navigate-top")) +}) + +test("test following a link within a frame with target=_top navigates the page", async ({ page }) => { + assert.equal(await attributeForSelector(page, "#navigate-top", "src"), null) + + await page.click("#navigate-top a:not([data-turbo-frame])") + await nextBeat() + + const frameText = await page.textContent("body > h1") + assert.equal(frameText, "One") + assert.notOk(await hasSelector(page, "#navigate-top a")) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await searchParams(page.url()).get("key"), "value") +}) + +test("test following a link that declares data-turbo-frame='_self' within a frame with target=_top navigates the frame itself", async ({ + page, +}) => { + assert.equal(await attributeForSelector(page, "#navigate-top", "src"), null) + + await page.click("#navigate-top a[data-turbo-frame='_self']") + await nextBeat() + + const title = await page.textContent("body > h1") + assert.equal(title, "Frames") + assert.ok(await hasSelector(page, "#navigate-top")) + const frame = await page.textContent("#navigate-top") + assert.equal(frame, "Replaced only the frame") +}) + +test("test following a link to a page with a which lazily loads a matching frame", async ({ + page, +}) => { + await nextBeat() + await page.click("#recursive summary") + assert.ok(await hasSelector(page, "#recursive details[open]")) + + await page.click("#recursive a") + await nextBeat() + assert.ok(await hasSelector(page, "#recursive details:not([open])")) +}) + +test("test submitting a form that redirects to a page with a which lazily loads a matching frame", async ({ + page, +}) => { + await nextBeat() + await page.click("#recursive summary") + assert.ok(await hasSelector(page, "#recursive details[open]")) + + await page.click("#recursive input[type=submit]") + await nextBeat() + assert.ok(await hasSelector(page, "#recursive details:not([open])")) +}) + +test("test removing [disabled] attribute from eager-loaded frame navigates it", async ({ page }) => { + await page.evaluate(() => document.getElementById("frame")?.setAttribute("disabled", "")) + await page.evaluate(() => + document.getElementById("frame")?.setAttribute("src", "/src/tests/fixtures/frames/frame.html") + ) + + assert.ok( + await noNextEventOnTarget(page, "frame", "turbo:before-fetch-request"), + "[disabled] frames do not submit requests" + ) + + await page.evaluate(() => document.getElementById("frame")?.removeAttribute("disabled")) + + await nextEventOnTarget(page, "frame", "turbo:before-fetch-request") +}) + +test("test evaluates frame script elements on each render", async ({ page }) => { + assert.equal(await frameScriptEvaluationCount(page), undefined) + + await page.click("#body-script-link") + assert.equal(await frameScriptEvaluationCount(page), 1) + + await page.click("#body-script-link") + assert.equal(await frameScriptEvaluationCount(page), 2) +}) + +test("test does not evaluate data-turbo-eval=false scripts", async ({ page }) => { + await page.click("#eval-false-script-link") + await nextBeat() + assert.equal(await frameScriptEvaluationCount(page), undefined) +}) + +test("test redirecting in a form is still navigatable after redirect", async ({ page }) => { + await nextBeat() + await page.click("#navigate-form-redirect") + await nextBeat() + assert.ok(await hasSelector(page, "#form-redirect")) + + await nextBeat() + await page.click("#submit-form") + await nextBeat() + assert.ok(await hasSelector(page, "#form-redirected-header")) + + await nextBeat() + await page.click("#navigate-form-redirect") + await nextBeat() + assert.ok(await hasSelector(page, "#form-redirect-header")) +}) + +test("test 'turbo:frame-render' is triggered after frame has finished rendering", async ({ page }) => { + await page.click("#frame-part") + + await nextEventNamed(page, "turbo:frame-render") // recursive + const { fetchResponse } = await nextEventNamed(page, "turbo:frame-render") + + assert.include(fetchResponse.response.url, "/src/tests/fixtures/frames/part.html") +}) + +test("test navigating a frame fires events", async ({ page }) => { + await page.click("#outside-frame-form") + + const { fetchResponse } = await nextEventOnTarget(page, "frame", "turbo:frame-render") + assert.include(fetchResponse.response.url, "/src/tests/fixtures/frames/form.html") + + await nextEventOnTarget(page, "frame", "turbo:frame-load") + + const otherEvents = await readEventLogs(page) + assert.equal(otherEvents.length, 0, "no more events") +}) + +test("test following inner link reloads frame on every click", async ({ page }) => { + await page.click("#hello a") + await nextEventNamed(page, "turbo:before-fetch-request") + + await page.click("#hello a") + await nextEventNamed(page, "turbo:before-fetch-request") +}) + +test("test following outer link reloads frame on every click", async ({ page }) => { + await page.click("#outer-frame-link") + await nextEventNamed(page, "turbo:before-fetch-request") + + await page.click("#outer-frame-link") + await nextEventNamed(page, "turbo:before-fetch-request") +}) + +test("test following outer form reloads frame on every submit", async ({ page }) => { + await page.click("#outer-frame-submit") + await nextEventNamed(page, "turbo:before-fetch-request") + + await page.click("#outer-frame-submit") + await nextEventNamed(page, "turbo:before-fetch-request") +}) - async "test an inner/outer link reloads frame on every click"() { - await this.clickSelector("#inner-outer-frame-link") - await this.nextEventNamed("turbo:before-fetch-request") +test("test an inner/outer link reloads frame on every click", async ({ page }) => { + await page.click("#inner-outer-frame-link") + await nextEventNamed(page, "turbo:before-fetch-request") - await this.clickSelector("#inner-outer-frame-link") - await this.nextEventNamed("turbo:before-fetch-request") - } + await page.click("#inner-outer-frame-link") + await nextEventNamed(page, "turbo:before-fetch-request") +}) - async "test an inner/outer form reloads frame on every submit"() { - await this.clickSelector("#inner-outer-frame-submit") - await this.nextEventNamed("turbo:before-fetch-request") - - await this.clickSelector("#inner-outer-frame-submit") - await this.nextEventNamed("turbo:before-fetch-request") - } +test("test an inner/outer form reloads frame on every submit", async ({ page }) => { + await page.click("#inner-outer-frame-submit") + await nextEventNamed(page, "turbo:before-fetch-request") - async "test reconnecting after following a link does not reload the frame"() { - await this.clickSelector("#hello a") - await this.nextEventNamed("turbo:before-fetch-request") + await page.click("#inner-outer-frame-submit") + await nextEventNamed(page, "turbo:before-fetch-request") +}) - await this.remote.execute(() => { - window.savedElement = document.querySelector("#frame") - window.savedElement?.remove() - }) - await this.nextBeat +test("test reconnecting after following a link does not reload the frame", async ({ page }) => { + await page.click("#hello a") + await nextEventNamed(page, "turbo:before-fetch-request") - await this.remote.execute(() => { - if (window.savedElement) { - document.body.appendChild(window.savedElement) + await page.evaluate(() => { + window.savedElement = document.querySelector("#frame") + window.savedElement?.remove() + }) + await nextBeat() + + await page.evaluate(() => { + if (window.savedElement) { + document.body.appendChild(window.savedElement) + } + }) + await nextBeat() + + const eventLogs = await readEventLogs(page) + const requestLogs = eventLogs.filter(([name]) => name == "turbo:before-fetch-request") + assert.equal(requestLogs.length, 0) +}) + +test("test navigating pushing URL state from a frame navigation fires events", async ({ page }) => { + await page.click("#link-outside-frame-action-advance") + + assert.equal( + await nextAttributeMutationNamed(page, "frame", "aria-busy"), + "true", + "sets aria-busy on the " + ) + await nextEventOnTarget(page, "frame", "turbo:before-fetch-request") + await nextEventOnTarget(page, "frame", "turbo:before-fetch-response") + await nextEventOnTarget(page, "frame", "turbo:frame-render") + await nextEventOnTarget(page, "frame", "turbo:frame-load") + assert.notOk(await nextAttributeMutationNamed(page, "frame", "aria-busy"), "removes aria-busy from the ") + + assert.equal(await nextAttributeMutationNamed(page, "html", "aria-busy"), "true", "sets aria-busy on the ") + await nextEventOnTarget(page, "html", "turbo:before-visit") + await nextEventOnTarget(page, "html", "turbo:visit") + await nextEventOnTarget(page, "html", "turbo:before-cache") + await nextEventOnTarget(page, "html", "turbo:before-render") + await nextEventOnTarget(page, "html", "turbo:render") + await nextEventOnTarget(page, "html", "turbo:load") + assert.notOk(await nextAttributeMutationNamed(page, "html", "aria-busy"), "removes aria-busy from the ") +}) + +test("test navigating a frame with a form[method=get] that does not redirect still updates the [src]", async ({ + page, +}) => { + await page.click("#frame-form-get-no-redirect") + await nextEventNamed(page, "turbo:before-fetch-request") + await nextEventNamed(page, "turbo:before-fetch-response") + await nextEventOnTarget(page, "frame", "turbo:frame-render") + await nextEventOnTarget(page, "frame", "turbo:frame-load") + await noNextEventNamed(page, "turbo:before-fetch-request") + + const src = (await attributeForSelector(page, "#frame", "src")) ?? "" + + assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") + assert.equal(await page.textContent("h1"), "Frames") + assert.equal(await page.textContent("#frame h2"), "Frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames.html") +}) + +test("test navigating turbo-frame[data-turbo-action=advance] from within pushes URL state", async ({ page }) => { + await page.click("#add-turbo-action-to-frame") + await page.click("#link-frame") + await nextEventNamed(page, "turbo:load") + + const title = await page.textContent("h1") + const frameTitle = await page.textContent("#frame h2") + + assert.equal(title, "Frames") + assert.equal(frameTitle, "Frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/frame.html") +}) + +test("test navigating turbo-frame[data-turbo-action=advance] to the same URL clears the [aria-busy] and [data-turbo-preview] state", async ({ + page, +}) => { + await page.click("#link-outside-frame-action-advance") + await nextEventNamed(page, "turbo:load") + await page.click("#link-outside-frame-action-advance") + await nextEventNamed(page, "turbo:load") + await page.click("#link-outside-frame-action-advance") + await nextEventNamed(page, "turbo:load") + + assert.equal(await attributeForSelector(page, "#frame", "aria-busy"), null, "clears turbo-frame[aria-busy]") + assert.equal(await attributeForSelector(page, "#html", "aria-busy"), null, "clears html[aria-busy]") + assert.equal(await attributeForSelector(page, "#html", "data-turbo-preview"), null, "clears html[aria-busy]") +}) + +test("test navigating a turbo-frame with an a[data-turbo-action=advance] preserves page state", async ({ page }) => { + await scrollToSelector(page, "#below-the-fold-input") + await page.fill("#below-the-fold-input", "a value") + await page.click("#below-the-fold-link-frame-action") + await nextEventNamed(page, "turbo:load") + + const title = await page.textContent("h1") + const frameTitle = await page.textContent("#frame h2") + const src = (await attributeForSelector(page, "#frame", "src")) ?? "" + + assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") + assert.equal(title, "Frames") + assert.equal(frameTitle, "Frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/frame.html") + assert.equal(await propertyForSelector(page, "#below-the-fold-input", "value"), "a value", "preserves page state") + + const { y } = await scrollPosition(page) + assert.notEqual(y, 0, "preserves Y scroll position") +}) + +test("test a turbo-frame that has been driven by a[data-turbo-action] can be navigated normally", async ({ page }) => { + await page.click("#remove-target-from-hello") + await page.click("#link-hello-advance") + await nextEventNamed(page, "turbo:load") + + assert.equal(await page.textContent("h1"), "Frames") + assert.equal(await page.textContent("#hello h2"), "Hello from a frame") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/hello.html") + + await page.click("#hello a") + await nextEventOnTarget(page, "hello", "turbo:frame-load") + await noNextEventNamed(page, "turbo:load") + + assert.equal(await page.textContent("#hello h2"), "Frames: #hello") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/hello.html") +}) + +test("test navigating turbo-frame from within with a[data-turbo-action=advance] pushes URL state", async ({ page }) => { + await page.click("#link-nested-frame-action-advance") + await nextEventNamed(page, "turbo:load") + + const title = await page.textContent("h1") + const frameTitle = await page.textContent("#frame h2") + const src = (await attributeForSelector(page, "#frame", "src")) ?? "" + + assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") + assert.equal(title, "Frames") + assert.equal(frameTitle, "Frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/frame.html") + assert.ok(await hasSelector(page, "#frame[complete]"), "marks the frame as [complete]") +}) + +test("test navigating frame with a[data-turbo-action=advance] pushes URL state", async ({ page }) => { + await page.click("#link-outside-frame-action-advance") + await nextEventNamed(page, "turbo:load") + + const title = await page.textContent("h1") + const frameTitle = await page.textContent("#frame h2") + const src = (await attributeForSelector(page, "#frame", "src")) ?? "" + + assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") + assert.equal(title, "Frames") + assert.equal(frameTitle, "Frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/frame.html") + assert.ok(await hasSelector(page, "#frame[complete]"), "marks the frame as [complete]") +}) + +test("test navigating frame with form[method=get][data-turbo-action=advance] pushes URL state", async ({ page }) => { + await page.click("#form-get-frame-action-advance button") + await nextEventNamed(page, "turbo:load") + + const title = await page.textContent("h1") + const frameTitle = await page.textContent("#frame h2") + const src = (await attributeForSelector(page, "#frame", "src")) ?? "" + + assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") + assert.equal(title, "Frames") + assert.equal(frameTitle, "Frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/frame.html") + assert.ok(await hasSelector(page, "#frame[complete]"), "marks the frame as [complete]") +}) + +test("test navigating frame with form[method=get][data-turbo-action=advance] to the same URL clears the [aria-busy] and [data-turbo-preview] state", async ({ + page, +}) => { + await page.click("#form-get-frame-action-advance button") + await nextEventNamed(page, "turbo:load") + await page.click("#form-get-frame-action-advance button") + await nextEventNamed(page, "turbo:load") + await page.click("#form-get-frame-action-advance button") + await nextEventNamed(page, "turbo:load") + + assert.equal(await attributeForSelector(page, "#frame", "aria-busy"), null, "clears turbo-frame[aria-busy]") + assert.equal(await attributeForSelector(page, "#html", "aria-busy"), null, "clears html[aria-busy]") + assert.equal(await attributeForSelector(page, "#html", "data-turbo-preview"), null, "clears html[aria-busy]") +}) + +test("test navigating frame with form[method=post][data-turbo-action=advance] pushes URL state", async ({ page }) => { + await page.click("#form-post-frame-action-advance button") + await nextEventNamed(page, "turbo:load") + + const title = await page.textContent("h1") + const frameTitle = await page.textContent("#frame h2") + const src = (await attributeForSelector(page, "#frame", "src")) ?? "" + + assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") + assert.equal(title, "Frames") + assert.equal(frameTitle, "Frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/frame.html") + assert.ok(await hasSelector(page, "#frame[complete]"), "marks the frame as [complete]") +}) + +test("test navigating frame with form[method=post][data-turbo-action=advance] to the same URL clears the [aria-busy] and [data-turbo-preview] state", async ({ + page, +}) => { + await page.click("#form-post-frame-action-advance button") + await nextEventNamed(page, "turbo:load") + await page.click("#form-post-frame-action-advance button") + await nextEventNamed(page, "turbo:load") + await page.click("#form-post-frame-action-advance button") + await nextEventNamed(page, "turbo:load") + + assert.equal(await attributeForSelector(page, "#frame", "aria-busy"), null, "clears turbo-frame[aria-busy]") + assert.equal(await attributeForSelector(page, "#html", "aria-busy"), null, "clears html[aria-busy]") + assert.equal(await attributeForSelector(page, "#html", "data-turbo-preview"), null, "clears html[aria-busy]") + assert.ok(await hasSelector(page, "#frame[complete]"), "marks the frame as [complete]") +}) + +test("test navigating frame with button[data-turbo-action=advance] pushes URL state", async ({ page }) => { + await page.click("#button-frame-action-advance") + await nextEventNamed(page, "turbo:load") + + const title = await page.textContent("h1") + const frameTitle = await page.textContent("#frame h2") + const src = (await attributeForSelector(page, "#frame", "src")) ?? "" + + assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") + assert.equal(title, "Frames") + assert.equal(frameTitle, "Frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/frame.html") + assert.ok(await hasSelector(page, "#frame[complete]"), "marks the frame as [complete]") +}) + +test("test navigating back after pushing URL state from a turbo-frame[data-turbo-action=advance] restores the frames previous contents", async ({ + page, +}) => { + await page.click("#add-turbo-action-to-frame") + await page.click("#link-frame") + await nextEventNamed(page, "turbo:load") + await page.goBack() + await nextEventNamed(page, "turbo:load") + + const title = await page.textContent("h1") + const frameTitle = await page.textContent("#frame h2") + + assert.equal(title, "Frames") + assert.equal(frameTitle, "Frames: #frame") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames.html") + assert.equal(await propertyForSelector(page, "#frame", "src"), null) +}) + +test("test navigating back then forward after pushing URL state from a turbo-frame[data-turbo-action=advance] restores the frames next contents", async ({ + page, +}) => { + await page.click("#add-turbo-action-to-frame") + await page.click("#link-frame") + await nextEventNamed(page, "turbo:load") + await page.goBack() + await nextEventNamed(page, "turbo:load") + await page.goForward() + await nextEventNamed(page, "turbo:load") + + const title = await page.textContent("h1") + const frameTitle = await page.textContent("#frame h2") + const src = (await attributeForSelector(page, "#frame", "src")) ?? "" + + assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") + assert.equal(title, "Frames") + assert.equal(frameTitle, "Frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/frame.html") + assert.ok(await hasSelector(page, "#frame[complete]"), "marks the frame as [complete]") +}) + +test("test turbo:before-fetch-request fires on the frame element", async ({ page }) => { + await page.click("#hello a") + assert.ok(await nextEventOnTarget(page, "frame", "turbo:before-fetch-request")) +}) + +test("test turbo:before-fetch-response fires on the frame element", async ({ page }) => { + await page.click("#hello a") + assert.ok(await nextEventOnTarget(page, "frame", "turbo:before-fetch-response")) +}) + +test("test navigating a eager frame with a link[method=get] that does not fetch eager frame twice", async ({ + page, +}) => { + await page.click("#link-to-eager-loaded-frame") + + await nextBeat() + + const eventLogs = await readEventLogs(page) + const fetchLogs = eventLogs.filter( + ([name, options]) => + name == "turbo:before-fetch-request" && options?.url?.includes("/src/tests/fixtures/frames/frame_for_eager.html") + ) + assert.equal(fetchLogs.length, 1) + + const src = (await attributeForSelector(page, "#eager-loaded-frame", "src")) ?? "" + assert.ok(src.includes("/src/tests/fixtures/frames/frame_for_eager.html"), "updates src attribute") + assert.equal(await page.textContent("h1"), "Eager-loaded frame") + assert.equal(await page.textContent("#eager-loaded-frame h2"), "Eager-loaded frame: Loaded") + assert.equal(pathname(page.url()), "/src/tests/fixtures/page_with_eager_frame.html") +}) + +async function withoutChangingEventListenersCount(page: Page, callback: () => Promise) { + const name = "eventListenersAttachedToDocument" + const setup = () => { + return page.evaluate((name) => { + const context = window as any + context[name] = 0 + context.originals = { + addEventListener: document.addEventListener, + removeEventListener: document.removeEventListener, } - }) - await this.nextBeat - - const eventLogs = await this.eventLogChannel.read() - const requestLogs = eventLogs.filter(([name]) => name == "turbo:before-fetch-request") - this.assert.equal(requestLogs.length, 0) - } - - async "test navigating pushing URL state from a frame navigation fires events"() { - await this.clickSelector("#link-outside-frame-action-advance") - - this.assert.equal( - await this.nextAttributeMutationNamed("frame", "aria-busy"), - "true", - "sets aria-busy on the " - ) - await this.nextEventOnTarget("frame", "turbo:before-fetch-request") - await this.nextEventOnTarget("frame", "turbo:before-fetch-response") - await this.nextEventOnTarget("frame", "turbo:frame-render") - await this.nextEventOnTarget("frame", "turbo:frame-load") - this.assert.notOk( - await this.nextAttributeMutationNamed("frame", "aria-busy"), - "removes aria-busy from the " - ) - - this.assert.equal( - await this.nextAttributeMutationNamed("html", "aria-busy"), - "true", - "sets aria-busy on the " - ) - await this.nextEventOnTarget("html", "turbo:before-visit") - await this.nextEventOnTarget("html", "turbo:visit") - await this.nextEventOnTarget("html", "turbo:before-cache") - await this.nextEventOnTarget("html", "turbo:before-render") - await this.nextEventOnTarget("html", "turbo:render") - await this.nextEventOnTarget("html", "turbo:load") - this.assert.notOk(await this.nextAttributeMutationNamed("html", "aria-busy"), "removes aria-busy from the ") - } - - async "test navigating a frame with a form[method=get] that does not redirect still updates the [src]"() { - await this.clickSelector("#frame-form-get-no-redirect") - await this.nextEventNamed("turbo:before-fetch-request") - await this.nextEventNamed("turbo:before-fetch-response") - await this.nextEventOnTarget("frame", "turbo:frame-render") - await this.nextEventOnTarget("frame", "turbo:frame-load") - await this.noNextEventNamed("turbo:before-fetch-request") - - const src = (await this.attributeForSelector("#frame", "src")) ?? "" - - this.assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") - this.assert.equal(await (await this.querySelector("h1")).getVisibleText(), "Frames") - this.assert.equal(await (await this.querySelector("#frame h2")).getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames.html") - } - - async "test navigating turbo-frame[data-turbo-action=advance] from within pushes URL state"() { - await this.clickSelector("#add-turbo-action-to-frame") - await this.clickSelector("#link-frame") - await this.nextEventNamed("turbo:load") - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#frame h2") - - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") - } - - async "test navigating turbo-frame[data-turbo-action=advance] to the same URL clears the [aria-busy] and [data-turbo-preview] state"() { - await this.clickSelector("#link-outside-frame-action-advance") - await this.nextEventNamed("turbo:load") - await this.clickSelector("#link-outside-frame-action-advance") - await this.nextEventNamed("turbo:load") - await this.clickSelector("#link-outside-frame-action-advance") - await this.nextEventNamed("turbo:load") - - this.assert.equal(await this.attributeForSelector("#frame", "aria-busy"), null, "clears turbo-frame[aria-busy]") - this.assert.equal(await this.attributeForSelector("#html", "aria-busy"), null, "clears html[aria-busy]") - this.assert.equal(await this.attributeForSelector("#html", "data-turbo-preview"), null, "clears html[aria-busy]") - } - - async "test navigating a turbo-frame with an a[data-turbo-action=advance] preserves page state"() { - await this.scrollToSelector("#below-the-fold-input") - await this.fillInSelector("#below-the-fold-input", "a value") - await this.clickSelector("#below-the-fold-link-frame-action") - await this.nextEventNamed("turbo:load") - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#frame h2") - const src = (await this.attributeForSelector("#frame", "src")) ?? "" - - this.assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") - this.assert.equal( - await this.propertyForSelector("#below-the-fold-input", "value"), - "a value", - "preserves page state" - ) - - const { y } = await this.scrollPosition - this.assert.notEqual(y, 0, "preserves Y scroll position") - } - - async "test a turbo-frame that has been driven by a[data-turbo-action] can be navigated normally"() { - await this.clickSelector("#remove-target-from-hello") - await this.clickSelector("#link-hello-advance") - await this.nextEventNamed("turbo:load") - - this.assert.equal(await (await this.querySelector("h1")).getVisibleText(), "Frames") - this.assert.equal(await (await this.querySelector("#hello h2")).getVisibleText(), "Hello from a frame") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/hello.html") - - await this.clickSelector("#hello a") - await this.nextEventOnTarget("hello", "turbo:frame-load") - await this.noNextEventNamed("turbo:load") - - this.assert.equal(await (await this.querySelector("#hello h2")).getVisibleText(), "Frames: #hello") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/hello.html") - } - - async "test navigating turbo-frame from within with a[data-turbo-action=advance] pushes URL state"() { - await this.clickSelector("#link-nested-frame-action-advance") - await this.nextEventNamed("turbo:load") - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#frame h2") - const src = (await this.attributeForSelector("#frame", "src")) ?? "" - - this.assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") - this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") - } - - async "test navigating frame with a[data-turbo-action=advance] pushes URL state"() { - await this.clickSelector("#link-outside-frame-action-advance") - await this.nextEventNamed("turbo:load") - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#frame h2") - const src = (await this.attributeForSelector("#frame", "src")) ?? "" - this.assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") - this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") - } - - async "test navigating frame with form[method=get][data-turbo-action=advance] pushes URL state"() { - await this.clickSelector("#form-get-frame-action-advance button") - await this.nextEventNamed("turbo:load") - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#frame h2") - const src = (await this.attributeForSelector("#frame", "src")) ?? "" - - this.assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") - this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") - } - - async "test navigating frame with form[method=get][data-turbo-action=advance] to the same URL clears the [aria-busy] and [data-turbo-preview] state"() { - await this.clickSelector("#form-get-frame-action-advance button") - await this.nextEventNamed("turbo:load") - await this.clickSelector("#form-get-frame-action-advance button") - await this.nextEventNamed("turbo:load") - await this.clickSelector("#form-get-frame-action-advance button") - await this.nextEventNamed("turbo:load") - - this.assert.equal(await this.attributeForSelector("#frame", "aria-busy"), null, "clears turbo-frame[aria-busy]") - this.assert.equal(await this.attributeForSelector("#html", "aria-busy"), null, "clears html[aria-busy]") - this.assert.equal(await this.attributeForSelector("#html", "data-turbo-preview"), null, "clears html[aria-busy]") - } - - async "test navigating frame with form[method=post][data-turbo-action=advance] pushes URL state"() { - await this.clickSelector("#form-post-frame-action-advance button") - await this.nextEventNamed("turbo:load") - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#frame h2") - const src = (await this.attributeForSelector("#frame", "src")) ?? "" - - this.assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") - this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") - } - - async "test navigating frame with form[method=post][data-turbo-action=advance] to the same URL clears the [aria-busy] and [data-turbo-preview] state"() { - await this.clickSelector("#form-post-frame-action-advance button") - await this.nextEventNamed("turbo:load") - await this.clickSelector("#form-post-frame-action-advance button") - await this.nextEventNamed("turbo:load") - await this.clickSelector("#form-post-frame-action-advance button") - await this.nextEventNamed("turbo:load") - - this.assert.equal(await this.attributeForSelector("#frame", "aria-busy"), null, "clears turbo-frame[aria-busy]") - this.assert.equal(await this.attributeForSelector("#html", "aria-busy"), null, "clears html[aria-busy]") - this.assert.equal(await this.attributeForSelector("#html", "data-turbo-preview"), null, "clears html[aria-busy]") - this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") - } - - async "test navigating frame with button[data-turbo-action=advance] pushes URL state"() { - await this.clickSelector("#button-frame-action-advance") - await this.nextEventNamed("turbo:load") - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#frame h2") - const src = (await this.attributeForSelector("#frame", "src")) ?? "" - - this.assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") - this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") - } - - async "test navigating back after pushing URL state from a turbo-frame[data-turbo-action=advance] restores the frames previous contents"() { - await this.clickSelector("#add-turbo-action-to-frame") - await this.clickSelector("#link-frame") - await this.nextEventNamed("turbo:load") - await this.goBack() - await this.nextEventNamed("turbo:load") - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#frame h2") + document.addEventListener = ( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ) => { + context.originals.addEventListener.call(document, type, listener, options) + context[name] += 1 + } - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.equal(await frameTitle.getVisibleText(), "Frames: #frame") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames.html") - this.assert.equal(await this.propertyForSelector("#frame", "src"), null) - } + document.removeEventListener = ( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ) => { + context.originals.removeEventListener.call(document, type, listener, options) + context[name] -= 1 + } - async "test navigating back then forward after pushing URL state from a turbo-frame[data-turbo-action=advance] restores the frames next contents"() { - await this.clickSelector("#add-turbo-action-to-frame") - await this.clickSelector("#link-frame") - await this.nextEventNamed("turbo:load") - await this.goBack() - await this.nextEventNamed("turbo:load") - await this.goForward() - await this.nextEventNamed("turbo:load") - - const title = await this.querySelector("h1") - const frameTitle = await this.querySelector("#frame h2") - const src = (await this.attributeForSelector("#frame", "src")) ?? "" - - this.assert.ok(src.includes("/src/tests/fixtures/frames/frame.html"), "updates src attribute") - this.assert.equal(await title.getVisibleText(), "Frames") - this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded") - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") - this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]") + return context[name] || 0 + }, name) } - async "test turbo:before-fetch-request fires on the frame element"() { - await this.clickSelector("#hello a") - this.assert.ok(await this.nextEventOnTarget("frame", "turbo:before-fetch-request")) - } + const teardown = () => { + return page.evaluate((name) => { + const context = window as any + const { addEventListener, removeEventListener } = context.originals - async "test turbo:before-fetch-response fires on the frame element"() { - await this.clickSelector("#hello a") - this.assert.ok(await this.nextEventOnTarget("frame", "turbo:before-fetch-response")) - } + document.addEventListener = addEventListener + document.removeEventListener = removeEventListener - async "test navigating a eager frame with a link[method=get] that does not fetch eager frame twice"() { - await this.clickSelector("#link-to-eager-loaded-frame") - - await this.nextBeat - - const eventLogs = await this.eventLogChannel.read() - const fetchLogs = eventLogs.filter( - ([name, options]) => - name == "turbo:before-fetch-request" && - options?.url?.includes("/src/tests/fixtures/frames/frame_for_eager.html") - ) - this.assert.equal(fetchLogs.length, 1) - - const src = (await this.attributeForSelector("#eager-loaded-frame", "src")) ?? "" - this.assert.ok(src.includes("/src/tests/fixtures/frames/frame_for_eager.html"), "updates src attribute") - this.assert.equal(await (await this.querySelector("h1")).getVisibleText(), "Eager-loaded frame") - this.assert.equal( - await (await this.querySelector("#eager-loaded-frame h2")).getVisibleText(), - "Eager-loaded frame: Loaded" - ) - this.assert.equal(await this.pathname, "/src/tests/fixtures/page_with_eager_frame.html") + return context[name] || 0 + }, name) } - async withoutChangingEventListenersCount(callback: () => void) { - const name = "eventListenersAttachedToDocument" - const setup = () => { - return this.evaluate( - (name: string) => { - const context = window as any - context[name] = 0 - context.originals = { - addEventListener: document.addEventListener, - removeEventListener: document.removeEventListener, - } - - document.addEventListener = ( - type: string, - listener: EventListenerOrEventListenerObject, - options?: boolean | AddEventListenerOptions - ) => { - context.originals.addEventListener.call(document, type, listener, options) - context[name] += 1 - } - - document.removeEventListener = ( - type: string, - listener: EventListenerOrEventListenerObject, - options?: boolean | AddEventListenerOptions - ) => { - context.originals.removeEventListener.call(document, type, listener, options) - context[name] -= 1 - } - - return context[name] || 0 - }, - [name] - ) - } - - const teardown = () => { - return this.evaluate( - (name: string) => { - const context = window as any - const { addEventListener, removeEventListener } = context.originals - - document.addEventListener = addEventListener - document.removeEventListener = removeEventListener - - return context[name] || 0 - }, - [name] - ) - } + const originalCount = await setup() + await callback() + const finalCount = await teardown() - const originalCount = await setup() - await callback() - const finalCount = await teardown() - - this.assert.equal(finalCount, originalCount, "expected callback not to leak event listeners") - } - - async fillInSelector(selector: string, value: string) { - const element = await this.querySelector(selector) - - await element.click() - - return element.type(value) - } + assert.equal(finalCount, originalCount, "expected callback not to leak event listeners") +} - get frameScriptEvaluationCount(): Promise { - return this.evaluate(() => window.frameScriptEvaluationCount) - } +function frameScriptEvaluationCount(page: Page): Promise { + return page.evaluate(() => window.frameScriptEvaluationCount) } declare global { @@ -685,5 +705,3 @@ declare global { frameScriptEvaluationCount?: number } } - -FrameTests.registerSuite() diff --git a/src/tests/functional/import_tests.ts b/src/tests/functional/import_tests.ts index ede73d2f8..e2cb745a0 100644 --- a/src/tests/functional/import_tests.ts +++ b/src/tests/functional/import_tests.ts @@ -1,13 +1,10 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" -export class ImportTests extends TurboDriveTestCase { - async "test window variable with ESM"() { - await this.goToLocation("/src/tests/fixtures/esm.html") - const type = await this.evaluate(() => { - return typeof window.Turbo - }) - this.assert.equal(type, "object") - } -} - -ImportTests.registerSuite() +test("test window variable with ESM", async ({ page }) => { + await page.goto("/src/tests/fixtures/esm.html") + const type = await page.evaluate(() => { + return typeof window.Turbo + }) + assert.equal(type, "object") +}) diff --git a/src/tests/functional/index.ts b/src/tests/functional/index.ts deleted file mode 100644 index 2cd8bbd4f..000000000 --- a/src/tests/functional/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -export * from "./async_script_tests" -export * from "./autofocus_tests" -export * from "./cache_observer_tests" -export * from "./drive_disabled_tests" -export * from "./drive_tests" -export * from "./form_submission_tests" -export * from "./frame_tests" -export * from "./import_tests" -export * from "./frame_navigation_tests" -export * from "./loading_tests" -export * from "./navigation_tests" -export * from "./pausable_rendering_tests" -export * from "./pausable_requests_tests" -export * from "./preloader_tests" -export * from "./rendering_tests" -export * from "./scroll_restoration_tests" -export * from "./stream_tests" -export * from "./visit_tests" diff --git a/src/tests/functional/loading_tests.ts b/src/tests/functional/loading_tests.ts index eb0c74ffc..4ea539518 100644 --- a/src/tests/functional/loading_tests.ts +++ b/src/tests/functional/loading_tests.ts @@ -1,4 +1,15 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" +import { + attributeForSelector, + hasSelector, + nextBeat, + nextBody, + nextEventNamed, + nextEventOnTarget, + noNextEventNamed, + readEventLogs, +} from "../helpers/page" declare global { interface Window { @@ -6,207 +17,199 @@ declare global { } } -export class LoadingTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/loading.html") - } - - async "test eager loading within a details element"() { - await this.nextBeat - this.assert.ok(await this.hasSelector("#loading-eager turbo-frame#frame h2")) - this.assert.ok(await this.hasSelector("#loading-eager turbo-frame[complete]"), "has [complete] attribute") - } - - async "test lazy loading within a details element"() { - await this.nextBeat - - const frameContents = "#loading-lazy turbo-frame h2" - this.assert.notOk(await this.hasSelector(frameContents)) - this.assert.ok(await this.hasSelector("#loading-lazy turbo-frame:not([complete])")) - - await this.clickSelector("#loading-lazy summary") - await this.nextBeat - - const contents = await this.querySelector(frameContents) - this.assert.equal(await contents.getVisibleText(), "Hello from a frame") - this.assert.ok(await this.hasSelector("#loading-lazy turbo-frame[complete]"), "has [complete] attribute") - } - - async "test changing loading attribute from lazy to eager loads frame"() { - const frameContents = "#loading-lazy turbo-frame h2" - await this.nextBeat - - this.assert.notOk(await this.hasSelector(frameContents)) - - await this.remote.execute(() => - document.querySelector("#loading-lazy turbo-frame")?.setAttribute("loading", "eager") - ) - await this.nextBeat - - const contents = await this.querySelector(frameContents) - await this.clickSelector("#loading-lazy summary") - this.assert.equal(await contents.getVisibleText(), "Hello from a frame") - } - - async "test navigating a visible frame with loading=lazy navigates"() { - await this.clickSelector("#loading-lazy summary") - await this.nextBeat - - const initialContents = await this.querySelector("#hello h2") - this.assert.equal(await initialContents.getVisibleText(), "Hello from a frame") - - await this.clickSelector("#hello a") - await this.nextBeat +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/loading.html") + await readEventLogs(page) +}) - const navigatedContents = await this.querySelector("#hello h2") - this.assert.equal(await navigatedContents.getVisibleText(), "Frames: #hello") - } - - async "test changing src attribute on a frame with loading=lazy defers navigation"() { - const frameContents = "#loading-lazy turbo-frame h2" - await this.nextBeat - - await this.remote.execute(() => - document.querySelector("#loading-lazy turbo-frame")?.setAttribute("src", "/src/tests/fixtures/frames.html") - ) - this.assert.notOk(await this.hasSelector(frameContents)) - - await this.clickSelector("#loading-lazy summary") - await this.nextBeat - - const contents = await this.querySelector(frameContents) - this.assert.equal(await contents.getVisibleText(), "Frames: #hello") - } - - async "test changing src attribute on a frame with loading=eager navigates"() { - const frameContents = "#loading-eager turbo-frame h2" - await this.nextBeat - - await this.remote.execute(() => - document.querySelector("#loading-eager turbo-frame")?.setAttribute("src", "/src/tests/fixtures/frames.html") - ) +test("test eager loading within a details element", async ({ page }) => { + await nextBeat() + assert.ok(await hasSelector(page, "#loading-eager turbo-frame#frame h2")) + assert.ok(await hasSelector(page, "#loading-eager turbo-frame[complete]"), "has [complete] attribute") +}) - await this.clickSelector("#loading-eager summary") - await this.nextBeat +test("test lazy loading within a details element", async ({ page }) => { + await nextBeat() - const contents = await this.querySelector(frameContents) - this.assert.equal(await contents.getVisibleText(), "Frames: #frame") - } + const frameContents = "#loading-lazy turbo-frame h2" + assert.notOk(await hasSelector(page, frameContents)) + assert.ok(await hasSelector(page, "#loading-lazy turbo-frame:not([complete])")) - async "test reloading a frame reloads the content"() { - await this.nextBeat + await page.click("#loading-lazy summary") + await nextBeat() - await this.clickSelector("#loading-eager summary") - await this.nextBeat + const contents = await page.locator(frameContents) + assert.equal(await contents.textContent(), "Hello from a frame") + assert.ok(await hasSelector(page, "#loading-lazy turbo-frame[complete]"), "has [complete] attribute") +}) - const frameContent = "#loading-eager turbo-frame#frame h2" - this.assert.ok(await this.hasSelector(frameContent)) - this.assert.ok(await this.hasSelector("#loading-eager turbo-frame[complete]"), "has [complete] attribute") +test("test changing loading attribute from lazy to eager loads frame", async ({ page }) => { + const frameContents = "#loading-lazy turbo-frame h2" + await nextBeat() - await this.remote.execute(() => (document.querySelector("#loading-eager turbo-frame") as any)?.reload()) - this.assert.ok(await this.hasSelector(frameContent)) - this.assert.ok(await this.hasSelector("#loading-eager turbo-frame:not([complete])"), "clears [complete] attribute") - } + assert.notOk(await hasSelector(page, frameContents)) - async "test navigating away from a page does not reload its frames"() { - await this.clickSelector("#one") - await this.nextBody + await page.evaluate(() => document.querySelector("#loading-lazy turbo-frame")?.setAttribute("loading", "eager")) + await nextBeat() - const eventLogs = await this.eventLogChannel.read() - const requestLogs = eventLogs.filter(([name]) => name == "turbo:before-fetch-request") - this.assert.equal(requestLogs.length, 1) - } + const contents = await page.locator(frameContents) + await page.click("#loading-lazy summary") + assert.equal(await contents.textContent(), "Hello from a frame") +}) - async "test removing the [complete] attribute of an eager frame reloads the content"() { - await this.nextEventOnTarget("frame", "turbo:frame-load") - await this.remote.execute(() => document.querySelector("#loading-eager turbo-frame")?.removeAttribute("complete")) - await this.nextEventOnTarget("frame", "turbo:frame-load") +test("test navigating a visible frame with loading=lazy navigates", async ({ page }) => { + await page.click("#loading-lazy summary") + await nextBeat() - this.assert.ok( - await this.hasSelector("#loading-eager turbo-frame[complete]"), - "sets the [complete] attribute after re-loading" - ) - } + const initialContents = await page.locator("#hello h2") + assert.equal(await initialContents.textContent(), "Hello from a frame") - async "test changing [src] attribute on a [complete] frame with loading=lazy defers navigation"() { - await this.nextEventOnTarget("frame", "turbo:frame-load") - await this.clickSelector("#loading-lazy summary") - await this.nextEventOnTarget("hello", "turbo:frame-load") + await page.click("#hello a") + await nextBeat() - this.assert.ok(await this.hasSelector("#loading-lazy turbo-frame[complete]"), "lazy frame is complete") - this.assert.equal(await (await this.querySelector("#hello h2")).getVisibleText(), "Hello from a frame") + const navigatedContents = await page.locator("#hello h2") + assert.equal(await navigatedContents.textContent(), "Frames: #hello") +}) - await this.clickSelector("#loading-lazy summary") - await this.clickSelector("#one") - await this.nextEventNamed("turbo:load") - await this.goBack() - await this.nextBody - await this.noNextEventNamed("turbo:frame-load") +test("test changing src attribute on a frame with loading=lazy defers navigation", async ({ page }) => { + const frameContents = "#loading-lazy turbo-frame h2" + await nextBeat() - let src = new URL((await this.attributeForSelector("#hello", "src")) || "") + await page.evaluate(() => + document.querySelector("#loading-lazy turbo-frame")?.setAttribute("src", "/src/tests/fixtures/frames.html") + ) + assert.notOk(await hasSelector(page, frameContents)) - this.assert.ok(await this.hasSelector("#loading-lazy turbo-frame[complete]"), "lazy frame is complete") - this.assert.equal(src.pathname, "/src/tests/fixtures/frames/hello.html", "lazy frame retains [src]") + await page.click("#loading-lazy summary") + await nextBeat() - await this.clickSelector("#link-lazy-frame") - await this.noNextEventNamed("turbo:frame-load") + const contents = await page.locator(frameContents) + assert.equal(await contents.textContent(), "Frames: #hello") +}) - this.assert.ok(await this.hasSelector("#loading-lazy turbo-frame:not([complete])"), "lazy frame is not complete") +test("test changing src attribute on a frame with loading=eager navigates", async ({ page }) => { + const frameContents = "#loading-eager turbo-frame h2" + await nextBeat() - await this.clickSelector("#loading-lazy summary") - await this.nextEventOnTarget("hello", "turbo:frame-load") + await page.evaluate(() => + document.querySelector("#loading-eager turbo-frame")?.setAttribute("src", "/src/tests/fixtures/frames.html") + ) - src = new URL((await this.attributeForSelector("#hello", "src")) || "") + await page.click("#loading-eager summary") + await nextBeat() - this.assert.equal( - await (await this.querySelector("#loading-lazy turbo-frame h2")).getVisibleText(), - "Frames: #hello" - ) - this.assert.ok(await this.hasSelector("#loading-lazy turbo-frame[complete]"), "lazy frame is complete") - this.assert.equal(src.pathname, "/src/tests/fixtures/frames.html", "lazy frame navigates") - } + const contents = await page.locator(frameContents) + assert.equal(await contents.textContent(), "Frames: #frame") +}) - async "test navigating away from a page and then back does not reload its frames"() { - await this.clickSelector("#one") - await this.nextBody - await this.eventLogChannel.read() - await this.goBack() - await this.nextBody - - const eventLogs = await this.eventLogChannel.read() - const requestLogs = eventLogs.filter(([name]) => name == "turbo:before-fetch-request") - const requestsOnEagerFrame = requestLogs.filter((record) => record[2] == "frame") - const requestsOnLazyFrame = requestLogs.filter((record) => record[2] == "hello") - - this.assert.equal(requestsOnEagerFrame.length, 0, "does not reload eager frame") - this.assert.equal(requestsOnLazyFrame.length, 0, "does not reload lazy frame") - - await this.clickSelector("#loading-lazy summary") - await this.nextEventOnTarget("hello", "turbo:before-fetch-request") - await this.nextEventOnTarget("hello", "turbo:frame-render") - await this.nextEventOnTarget("hello", "turbo:frame-load") - } +test("test reloading a frame reloads the content", async ({ page }) => { + await nextBeat() - async "test disconnecting and reconnecting a frame does not reload the frame"() { - await this.nextBeat - - await this.remote.execute(() => { - window.savedElement = document.querySelector("#loading-eager") - window.savedElement?.remove() - }) - await this.nextBeat - - await this.remote.execute(() => { - if (window.savedElement) { - document.body.appendChild(window.savedElement) - } - }) - await this.nextBeat - - const eventLogs = await this.eventLogChannel.read() - const requestLogs = eventLogs.filter(([name]) => name == "turbo:before-fetch-request") - this.assert.equal(requestLogs.length, 0) - } -} + await page.click("#loading-eager summary") + await nextBeat() + + const frameContent = "#loading-eager turbo-frame#frame h2" + assert.ok(await hasSelector(page, frameContent)) + assert.ok(await hasSelector(page, "#loading-eager turbo-frame[complete]"), "has [complete] attribute") + + await page.evaluate(() => (document.querySelector("#loading-eager turbo-frame") as any)?.reload()) + assert.ok(await hasSelector(page, frameContent)) + assert.ok(await hasSelector(page, "#loading-eager turbo-frame:not([complete])"), "clears [complete] attribute") +}) + +test("test navigating away from a page does not reload its frames", async ({ page }) => { + await page.click("#one") + await nextBody(page) + + const eventLogs = await readEventLogs(page) + const requestLogs = eventLogs.filter(([name]) => name == "turbo:before-fetch-request") + assert.equal(requestLogs.length, 1) +}) + +test("test removing the [complete] attribute of an eager frame reloads the content", async ({ page }) => { + await nextEventOnTarget(page, "frame", "turbo:frame-load") + await page.evaluate(() => document.querySelector("#loading-eager turbo-frame")?.removeAttribute("complete")) + await nextEventOnTarget(page, "frame", "turbo:frame-load") -LoadingTests.registerSuite() + assert.ok( + await hasSelector(page, "#loading-eager turbo-frame[complete]"), + "sets the [complete] attribute after re-loading" + ) +}) + +test("test changing [src] attribute on a [complete] frame with loading=lazy defers navigation", async ({ page }) => { + await nextEventOnTarget(page, "frame", "turbo:frame-load") + await page.click("#loading-lazy summary") + await nextEventOnTarget(page, "hello", "turbo:frame-load") + + assert.ok(await hasSelector(page, "#loading-lazy turbo-frame[complete]"), "lazy frame is complete") + assert.equal(await page.textContent("#hello h2"), "Hello from a frame") + + await page.click("#loading-lazy summary") + await page.click("#one") + await nextEventNamed(page, "turbo:load") + await page.goBack() + await nextBody(page) + await noNextEventNamed(page, "turbo:frame-load") + + let src = new URL((await attributeForSelector(page, "#hello", "src")) || "") + + assert.ok(await hasSelector(page, "#loading-lazy turbo-frame[complete]"), "lazy frame is complete") + assert.equal(src.pathname, "/src/tests/fixtures/frames/hello.html", "lazy frame retains [src]") + + await page.click("#link-lazy-frame") + await noNextEventNamed(page, "turbo:frame-load") + + assert.ok(await hasSelector(page, "#loading-lazy turbo-frame:not([complete])"), "lazy frame is not complete") + + await page.click("#loading-lazy summary") + await nextEventOnTarget(page, "hello", "turbo:frame-load") + + src = new URL((await attributeForSelector(page, "#hello", "src")) || "") + + assert.equal(await page.textContent("#loading-lazy turbo-frame h2"), "Frames: #hello") + assert.ok(await hasSelector(page, "#loading-lazy turbo-frame[complete]"), "lazy frame is complete") + assert.equal(src.pathname, "/src/tests/fixtures/frames.html", "lazy frame navigates") +}) + +test("test navigating away from a page and then back does not reload its frames", async ({ page }) => { + await page.click("#one") + await nextBody(page) + await readEventLogs(page) + await page.goBack() + await nextBody(page) + + const eventLogs = await readEventLogs(page) + const requestLogs = eventLogs.filter(([name]) => name == "turbo:before-fetch-request") + const requestsOnEagerFrame = requestLogs.filter((record) => record[2] == "frame") + const requestsOnLazyFrame = requestLogs.filter((record) => record[2] == "hello") + + assert.equal(requestsOnEagerFrame.length, 0, "does not reload eager frame") + assert.equal(requestsOnLazyFrame.length, 0, "does not reload lazy frame") + + await page.click("#loading-lazy summary") + await nextEventOnTarget(page, "hello", "turbo:before-fetch-request") + await nextEventOnTarget(page, "hello", "turbo:frame-render") + await nextEventOnTarget(page, "hello", "turbo:frame-load") +}) + +test("test disconnecting and reconnecting a frame does not reload the frame", async ({ page }) => { + await nextBeat() + + await page.evaluate(() => { + window.savedElement = document.querySelector("#loading-eager") + window.savedElement?.remove() + }) + await nextBeat() + + await page.evaluate(() => { + if (window.savedElement) { + document.body.appendChild(window.savedElement) + } + }) + await nextBeat() + + const eventLogs = await readEventLogs(page) + const requestLogs = eventLogs.filter(([name]) => name == "turbo:before-fetch-request") + assert.equal(requestLogs.length, 0) +}) diff --git a/src/tests/functional/navigation_tests.ts b/src/tests/functional/navigation_tests.ts index 9544c7a33..b5e97fb06 100644 --- a/src/tests/functional/navigation_tests.ts +++ b/src/tests/functional/navigation_tests.ts @@ -1,333 +1,350 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" - -export class NavigationTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/navigation.html") - } - - async "test navigating renders a progress bar"() { - const styleElement = await this.querySelector("style") - - this.assert.equal( - await styleElement.getProperty("nonce"), - "123", - "renders progress bar stylesheet inline with nonce" - ) - - await this.remote.execute(() => window.Turbo.setProgressBarDelay(0)) - await this.clickSelector("#delayed-link") - - await this.waitUntilSelector(".turbo-progress-bar") - this.assert.ok(await this.hasSelector(".turbo-progress-bar"), "displays progress bar") - - await this.nextEventNamed("turbo:load") - await this.waitUntilNoSelector(".turbo-progress-bar") - - this.assert.notOk(await this.hasSelector(".turbo-progress-bar"), "hides progress bar") - } - - async "test navigating does not render a progress bar before expiring the delay"() { - await this.remote.execute(() => window.Turbo.setProgressBarDelay(1000)) - await this.clickSelector("#same-origin-unannotated-link") - - this.assert.notOk(await this.hasSelector(".turbo-progress-bar"), "does not show progress bar before delay") - } - - async "test after loading the page"() { - this.assert.equal(await this.pathname, "/src/tests/fixtures/navigation.html") - this.assert.equal(await this.visitAction, "load") - } - - async "test following a same-origin unannotated link"() { - this.clickSelector("#same-origin-unannotated-link") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "advance") - this.assert.equal( - await this.nextAttributeMutationNamed("html", "aria-busy"), - "true", - "sets [aria-busy] on the document element" - ) - this.assert.equal( - await this.nextAttributeMutationNamed("html", "aria-busy"), - null, - "removes [aria-busy] from the document element" - ) - } - - async "test following a same-origin unannotated custom element link"() { - await this.nextBeat - await this.remote.execute(() => { - const shadowRoot = document.querySelector("#custom-link-element")?.shadowRoot - const link = shadowRoot?.querySelector("a") - link?.click() +import { test } from "@playwright/test" +import { assert } from "chai" +import { + clickWithoutScrolling, + hash, + hasSelector, + isScrolledToSelector, + nextAttributeMutationNamed, + nextBeat, + nextBody, + nextEventNamed, + noNextEventNamed, + pathname, + search, + selectorHasFocus, + visitAction, + waitUntilSelector, + waitUntilNoSelector, + willChangeBody, +} from "../helpers/page" + +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/navigation.html") +}) + +test("test navigating renders a progress bar", async ({ page }) => { + assert.equal( + await page.locator("style").evaluate((style) => style.nonce), + "123", + "renders progress bar stylesheet inline with nonce" + ) + + await page.evaluate(() => window.Turbo.setProgressBarDelay(0)) + await page.click("#delayed-link") + + await waitUntilSelector(page, ".turbo-progress-bar") + assert.ok(await hasSelector(page, ".turbo-progress-bar"), "displays progress bar") + + await nextEventNamed(page, "turbo:load") + await waitUntilNoSelector(page, ".turbo-progress-bar") + + assert.notOk(await hasSelector(page, ".turbo-progress-bar"), "hides progress bar") +}) + +test("test navigating does not render a progress bar before expiring the delay", async ({ page }) => { + await page.evaluate(() => window.Turbo.setProgressBarDelay(1000)) + await page.click("#same-origin-unannotated-link") + + assert.notOk(await hasSelector(page, ".turbo-progress-bar"), "does not show progress bar before delay") +}) + +test("test after loading the page", async ({ page }) => { + assert.equal(pathname(page.url()), "/src/tests/fixtures/navigation.html") + assert.equal(await visitAction(page), "load") +}) + +test("test following a same-origin unannotated link", async ({ page }) => { + page.click("#same-origin-unannotated-link") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "advance") + assert.equal( + await nextAttributeMutationNamed(page, "html", "aria-busy"), + "true", + "sets [aria-busy] on the document element" + ) + assert.equal( + await nextAttributeMutationNamed(page, "html", "aria-busy"), + null, + "removes [aria-busy] from the document element" + ) +}) + +test("test following a same-origin unannotated custom element link", async ({ page }) => { + await nextBeat() + await page.evaluate(() => { + const shadowRoot = document.querySelector("#custom-link-element")?.shadowRoot + const link = shadowRoot?.querySelector("a") + link?.click() + }) + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(search(page.url()), "") + assert.equal(await visitAction(page), "advance") +}) + +test("test following a same-origin unannotated link with search params", async ({ page }) => { + page.click("#same-origin-unannotated-link-search-params") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(search(page.url()), "?key=value") + assert.equal(await visitAction(page), "advance") +}) + +test("test following a same-origin unannotated form[method=GET]", async ({ page }) => { + page.click("#same-origin-unannotated-form button") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "advance") +}) + +test("test following a same-origin data-turbo-action=replace link", async ({ page }) => { + page.click("#same-origin-replace-link") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "replace") +}) + +test("test following a same-origin GET form[data-turbo-action=replace]", async ({ page }) => { + page.click("#same-origin-replace-form-get button") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "replace") +}) + +test("test following a same-origin GET form button[data-turbo-action=replace]", async ({ page }) => { + page.click("#same-origin-replace-form-submitter-get button") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "replace") +}) + +test("test following a same-origin POST form[data-turbo-action=replace]", async ({ page }) => { + page.click("#same-origin-replace-form-post button") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "replace") +}) + +test("test following a same-origin POST form button[data-turbo-action=replace]", async ({ page }) => { + page.click("#same-origin-replace-form-submitter-post button") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "replace") +}) + +test("test following a same-origin data-turbo=false link", async ({ page }) => { + page.click("#same-origin-false-link") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "load") +}) + +test("test following a same-origin unannotated link inside a data-turbo=false container", async ({ page }) => { + page.click("#same-origin-unannotated-link-inside-false-container") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "load") +}) + +test("test following a same-origin data-turbo=true link inside a data-turbo=false container", async ({ page }) => { + page.click("#same-origin-true-link-inside-false-container") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "advance") +}) + +test("test following a same-origin anchored link", async ({ page }) => { + await page.click("#same-origin-anchored-link") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(hash(page.url()), "#element-id") + assert.equal(await visitAction(page), "advance") + assert(await isScrolledToSelector(page, "#element-id")) +}) + +test("test following a same-origin link to a named anchor", async ({ page }) => { + await page.click("#same-origin-anchored-link-named") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(hash(page.url()), "#named-anchor") + assert.equal(await visitAction(page), "advance") + assert(await isScrolledToSelector(page, "[name=named-anchor]")) +}) + +test("test following a cross-origin unannotated link", async ({ page }) => { + await page.click("#cross-origin-unannotated-link") + await nextBody(page) + assert.equal(page.url(), "about:blank") + assert.equal(await visitAction(page), "load") +}) + +test("test following a same-origin [target] link", async ({ page }) => { + const [popup] = await Promise.all([page.waitForEvent("popup"), page.click("#same-origin-targeted-link")]) + + assert.equal(pathname(popup.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(popup), "load") +}) + +test("test following a same-origin [download] link", async ({ page }) => { + assert.notOk( + await willChangeBody(page, async () => { + await page.click("#same-origin-download-link") + await nextBeat() }) - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.search, "") - this.assert.equal(await this.visitAction, "advance") - } - - async "test following a same-origin unannotated link with search params"() { - this.clickSelector("#same-origin-unannotated-link-search-params") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.search, "?key=value") - this.assert.equal(await this.visitAction, "advance") - } - - async "test following a same-origin unannotated form[method=GET]"() { - this.clickSelector("#same-origin-unannotated-form button") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "advance") - } - - async "test following a same-origin data-turbo-action=replace link"() { - this.clickSelector("#same-origin-replace-link") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "replace") - } - - async "test following a same-origin GET form[data-turbo-action=replace]"() { - this.clickSelector("#same-origin-replace-form-get button") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "replace") - } - - async "test following a same-origin GET form button[data-turbo-action=replace]"() { - this.clickSelector("#same-origin-replace-form-submitter-get button") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "replace") - } - - async "test following a same-origin POST form[data-turbo-action=replace]"() { - this.clickSelector("#same-origin-replace-form-post button") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "replace") - } - - async "test following a same-origin POST form button[data-turbo-action=replace]"() { - this.clickSelector("#same-origin-replace-form-submitter-post button") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "replace") - } - - async "test following a same-origin data-turbo=false link"() { - this.clickSelector("#same-origin-false-link") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "load") - } - - async "test following a same-origin unannotated link inside a data-turbo=false container"() { - this.clickSelector("#same-origin-unannotated-link-inside-false-container") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "load") - } - - async "test following a same-origin data-turbo=true link inside a data-turbo=false container"() { - this.clickSelector("#same-origin-true-link-inside-false-container") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "advance") - } - - async "test following a same-origin anchored link"() { - this.clickSelector("#same-origin-anchored-link") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.hash, "#element-id") - this.assert.equal(await this.visitAction, "advance") - this.assert(await this.isScrolledToSelector("#element-id")) - } - - async "test following a same-origin link to a named anchor"() { - this.clickSelector("#same-origin-anchored-link-named") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.hash, "#named-anchor") - this.assert.equal(await this.visitAction, "advance") - this.assert(await this.isScrolledToSelector("[name=named-anchor]")) - } - - async "test following a cross-origin unannotated link"() { - this.clickSelector("#cross-origin-unannotated-link") - await this.nextBody - this.assert.equal(await this.location, "about:blank") - this.assert.equal(await this.visitAction, "load") - } - - async "test following a same-origin [target] link"() { - this.clickSelector("#same-origin-targeted-link") - await this.nextBeat - this.remote.switchToWindow(await this.nextWindowHandle) - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "load") - } - - async "test following a same-origin [download] link"() { - this.clickSelector("#same-origin-download-link") - await this.nextBeat - this.assert(!(await this.changedBody)) - this.assert.equal(await this.pathname, "/src/tests/fixtures/navigation.html") - this.assert.equal(await this.visitAction, "load") - } - - async "test following a same-origin link inside an SVG element"() { - this.clickSelector("#same-origin-link-inside-svg-element") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "advance") - } - - async "test following a cross-origin link inside an SVG element"() { - this.clickSelector("#cross-origin-link-inside-svg-element") - await this.nextBody - this.assert.equal(await this.location, "about:blank") - this.assert.equal(await this.visitAction, "load") - } - - async "test clicking the back button"() { - this.clickSelector("#same-origin-unannotated-link") - await this.nextBody - await this.goBack() - this.assert.equal(await this.pathname, "/src/tests/fixtures/navigation.html") - this.assert.equal(await this.visitAction, "restore") - } - - async "test clicking the forward button"() { - this.clickSelector("#same-origin-unannotated-link") - await this.nextBody - await this.goBack() - await this.goForward() - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "restore") - } - - async "test link targeting a disabled turbo-frame navigates the page"() { - await this.clickSelector("#link-to-disabled-frame") - await this.nextBody - - this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/hello.html") - } - - async "test skip link with hash-only path scrolls to the anchor without a visit"() { - const bodyElementId = (await this.body).elementId - await this.clickSelector('a[href="#main"]') - await this.nextBeat - - this.assert.equal((await this.body).elementId, bodyElementId, "does not reload page") - this.assert.ok(await this.isScrolledToSelector("#main"), "scrolled to #main") - } - - async "test skip link with hash-only path moves focus and changes tab order"() { - await this.clickSelector('a[href="#main"]') - await this.nextBeat - await this.pressTab() - - this.assert.notOk(await this.selectorHasFocus("#ignored-link"), "skips interactive elements before #main") - this.assert.ok( - await this.selectorHasFocus("#main a:first-of-type"), - "skips to first interactive element after #main" - ) - } - - async "test same-page anchored replace link assumes the intention was a refresh"() { - await this.clickSelector("#refresh-link") - await this.nextBody - this.assert.ok(await this.isScrolledToSelector("#main"), "scrolled to #main") - } - - async "test navigating back to anchored URL"() { - await this.clickSelector('a[href="#main"]') - await this.nextBeat - - await this.clickSelector("#same-origin-unannotated-link") - await this.nextBody - await this.nextBeat - - await this.goBack() - await this.nextBody - - this.assert.ok(await this.isScrolledToSelector("#main"), "scrolled to #main") - } - - async "test following a redirection"() { - await this.clickSelector("#redirection-link") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html") - this.assert.equal(await this.visitAction, "replace") - } - - async "test clicking the back button after redirection"() { - await this.clickSelector("#redirection-link") - await this.nextBody - await this.goBack() - this.assert.equal(await this.pathname, "/src/tests/fixtures/navigation.html") - this.assert.equal(await this.visitAction, "restore") - } - - async "test same-page anchor visits do not trigger visit events"() { - const events = [ - "turbo:before-visit", - "turbo:visit", - "turbo:before-cache", - "turbo:before-render", - "turbo:render", - "turbo:load", - ] - - for (const eventName in events) { - await this.goToLocation("/src/tests/fixtures/navigation.html") - await this.clickSelector('a[href="#main"]') - this.assert.ok(await this.noNextEventNamed(eventName), `same-page links do not trigger ${eventName} events`) - } - } - - async "test correct referrer header"() { - this.clickSelector("#headers-link") - await this.nextBody - const pre = await this.querySelector("pre") - const headers = await JSON.parse(await pre.getVisibleText()) - this.assert.equal( - headers.referer, - "http://localhost:9000/src/tests/fixtures/navigation.html", - `referer header is correctly set` - ) - } - - async "test double-clicking on a link"() { - this.clickSelector("#delayed-link") - this.clickSelector("#delayed-link") - - await this.nextBody - this.assert.equal(await this.pathname, "/__turbo/delayed_response") - this.assert.equal(await this.visitAction, "advance") - } - - async "test navigating back whilst a visit is in-flight"() { - this.clickSelector("#delayed-link") - await this.nextBeat - await this.goBack() - - this.assert.ok( - await this.nextEventNamed("turbo:visit"), - "navigating back whilst a visit is in-flight starts a non-silent Visit" - ) - - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/navigation.html") - this.assert.equal(await this.visitAction, "restore") - } -} - -NavigationTests.registerSuite() + ) + assert.equal(pathname(page.url()), "/src/tests/fixtures/navigation.html") + assert.equal(await visitAction(page), "load") +}) + +test("test following a same-origin link inside an SVG element", async ({ page }) => { + await page.click("#same-origin-link-inside-svg-element", { force: true }) + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "advance") +}) + +test("test following a cross-origin link inside an SVG element", async ({ page }) => { + await page.click("#cross-origin-link-inside-svg-element", { force: true }) + await nextBody(page) + assert.equal(page.url(), "about:blank") + assert.equal(await visitAction(page), "load") +}) + +test("test clicking the back button", async ({ page }) => { + await page.click("#same-origin-unannotated-link") + await nextBody(page) + await page.goBack() + assert.equal(pathname(page.url()), "/src/tests/fixtures/navigation.html") + assert.equal(await visitAction(page), "restore") +}) + +test("test clicking the forward button", async ({ page }) => { + await page.click("#same-origin-unannotated-link") + await nextBody(page) + await page.goBack() + await page.goForward() + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "restore") +}) + +test("test link targeting a disabled turbo-frame navigates the page", async ({ page }) => { + await page.click("#link-to-disabled-frame") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/hello.html") +}) + +test("test skip link with hash-only path scrolls to the anchor without a visit", async ({ page }) => { + assert.notOk( + await willChangeBody(page, async () => { + await page.click('a[href="#main"]') + await nextBeat() + }) + ) + + assert.ok(await isScrolledToSelector(page, "#main"), "scrolled to #main") +}) + +test("test skip link with hash-only path moves focus and changes tab order", async ({ page }) => { + await page.click('a[href="#main"]') + await nextBeat() + await page.press("#main", "Tab") + + assert.notOk(await selectorHasFocus(page, "#ignored-link"), "skips interactive elements before #main") + assert.ok( + await selectorHasFocus(page, "#same-origin-unannotated-link"), + "skips to first interactive element after #main" + ) +}) + +test("test same-page anchored replace link assumes the intention was a refresh", async ({ page }) => { + await page.click("#refresh-link") + await nextBody(page) + assert.ok(await isScrolledToSelector(page, "#main"), "scrolled to #main") +}) + +test("test navigating back to anchored URL", async ({ page }) => { + await clickWithoutScrolling(page, 'a[href="#main"]', { hasText: "Skip Link" }) + await nextBeat() + + await clickWithoutScrolling(page, "#same-origin-unannotated-link") + await nextBody(page) + await nextBeat() + + await page.goBack() + await nextBody(page) + + assert.ok(await isScrolledToSelector(page, "#main"), "scrolled to #main") +}) + +test("test following a redirection", async ({ page }) => { + await page.click("#redirection-link") + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") + assert.equal(await visitAction(page), "replace") +}) + +test("test clicking the back button after redirection", async ({ page }) => { + await page.click("#redirection-link") + await nextBody(page) + await page.goBack() + assert.equal(pathname(page.url()), "/src/tests/fixtures/navigation.html") + assert.equal(await visitAction(page), "restore") +}) + +test("test same-page anchor visits do not trigger visit events", async ({ page }) => { + const events = [ + "turbo:before-visit", + "turbo:visit", + "turbo:before-cache", + "turbo:before-render", + "turbo:render", + "turbo:load", + ] + + for (const eventName in events) { + await page.goto("/src/tests/fixtures/navigation.html") + await page.click('a[href="#main"]') + assert.ok(await noNextEventNamed(page, eventName), `same-page links do not trigger ${eventName} events`) + } +}) + +test("test correct referrer header", async ({ page }) => { + page.click("#headers-link") + await nextBody(page) + const pre = await page.textContent("pre") + const headers = await JSON.parse(pre || "") + assert.equal( + headers.referer, + "http://localhost:9000/src/tests/fixtures/navigation.html", + `referer header is correctly set` + ) +}) + +test("test double-clicking on a link", async ({ page }) => { + page.click("#delayed-link") + page.click("#delayed-link") + + await nextBody(page, 1200) + assert.equal(pathname(page.url()), "/__turbo/delayed_response") + assert.equal(await visitAction(page), "advance") +}) + +test("test navigating back whilst a visit is in-flight", async ({ page }) => { + page.click("#delayed-link") + await nextEventNamed(page, "turbo:before-render") + await page.goBack() + + assert.ok( + await nextEventNamed(page, "turbo:visit"), + "navigating back whilst a visit is in-flight starts a non-silent Visit" + ) + + await nextBody(page) + assert.equal(pathname(page.url()), "/src/tests/fixtures/navigation.html") + assert.equal(await visitAction(page), "restore") +}) diff --git a/src/tests/functional/pausable_rendering_tests.ts b/src/tests/functional/pausable_rendering_tests.ts index bfa9594c6..2e3c307a0 100644 --- a/src/tests/functional/pausable_rendering_tests.ts +++ b/src/tests/functional/pausable_rendering_tests.ts @@ -1,37 +1,34 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" +import { nextBeat } from "../helpers/page" -export class PausableRenderingTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/pausable_rendering.html") - } +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/pausable_rendering.html") +}) - async "test pauses and resumes rendering"() { - await this.clickSelector("#link") +test("test pauses and resumes rendering", async ({ page }) => { + page.on("dialog", (dialog) => { + assert.strictEqual(dialog.message(), "Continue rendering?") + dialog.accept() + }) - await this.nextBeat - this.assert.strictEqual(await this.getAlertText(), "Continue rendering?") - await this.acceptAlert() + await page.click("#link") + await nextBeat() - await this.nextBeat - const h1 = await this.querySelector("h1") - this.assert.equal(await h1.getVisibleText(), "One") - } + assert.equal(await page.textContent("h1"), "One") +}) - async "test aborts rendering"() { - await this.clickSelector("#link") +test("test aborts rendering", async ({ page }) => { + const [firstDialog] = await Promise.all([page.waitForEvent("dialog"), page.click("#link")]) - await this.nextBeat - this.assert.strictEqual(await this.getAlertText(), "Continue rendering?") - await this.dismissAlert() + assert.strictEqual(firstDialog.message(), "Continue rendering?") - await this.nextBeat - this.assert.strictEqual(await this.getAlertText(), "Rendering aborted") - await this.acceptAlert() + firstDialog.dismiss() - await this.nextBeat - const h1 = await this.querySelector("h1") - this.assert.equal(await h1.getVisibleText(), "Pausable Rendering") - } -} + const nextDialog = await page.waitForEvent("dialog") -PausableRenderingTests.registerSuite() + assert.strictEqual(nextDialog.message(), "Rendering aborted") + nextDialog.accept() + + assert.equal(await page.textContent("h1"), "Pausable Rendering") +}) diff --git a/src/tests/functional/pausable_requests_tests.ts b/src/tests/functional/pausable_requests_tests.ts index 630cc981f..b7f330758 100644 --- a/src/tests/functional/pausable_requests_tests.ts +++ b/src/tests/functional/pausable_requests_tests.ts @@ -1,37 +1,38 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" +import { nextBeat } from "../helpers/page" -export class PausableRequestsTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/pausable_requests.html") - } +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/pausable_requests.html") +}) - async "test pauses and resumes request"() { - await this.clickSelector("#link") +test("test pauses and resumes request", async ({ page }) => { + page.once("dialog", (dialog) => { + assert.strictEqual(dialog.message(), "Continue request?") + dialog.accept() + }) - await this.nextBeat - this.assert.strictEqual(await this.getAlertText(), "Continue request?") - await this.acceptAlert() + await page.click("#link") + await nextBeat() - await this.nextBeat - const h1 = await this.querySelector("h1") - this.assert.equal(await h1.getVisibleText(), "One") - } + assert.equal(await page.textContent("h1"), "One") +}) - async "test aborts request"() { - await this.clickSelector("#link") +test("test aborts request", async ({ page }) => { + page.once("dialog", (dialog) => { + assert.strictEqual(dialog.message(), "Continue request?") + dialog.dismiss() + }) - await this.nextBeat - this.assert.strictEqual(await this.getAlertText(), "Continue request?") - await this.dismissAlert() + await page.click("#link") + await nextBeat() - await this.nextBeat - this.assert.strictEqual(await this.getAlertText(), "Request aborted") - await this.acceptAlert() + page.once("dialog", (dialog) => { + assert.strictEqual(dialog.message(), "Request aborted") + dialog.accept() + }) - await this.nextBeat - const h1 = await this.querySelector("h1") - this.assert.equal(await h1.getVisibleText(), "Pausable Requests") - } -} + await nextBeat() -PausableRequestsTests.registerSuite() + assert.equal(await page.textContent("h1"), "Pausable Requests") +}) diff --git a/src/tests/functional/preloader_tests.ts b/src/tests/functional/preloader_tests.ts index 36c961fb4..3faac3dfd 100644 --- a/src/tests/functional/preloader_tests.ts +++ b/src/tests/functional/preloader_tests.ts @@ -1,55 +1,53 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" - -export class PreloaderTests extends TurboDriveTestCase { - async "test preloads snapshot on initial load"() { - // contains `a[rel="preload"][href="http://localhost:9000/src/tests/fixtures/preloaded.html"]` - await this.goToLocation("/src/tests/fixtures/preloading.html") - await this.nextBeat - - this.assert.ok( - await this.remote.execute(() => { - const preloadedUrl = "http://localhost:9000/src/tests/fixtures/preloaded.html" - const cache = window.Turbo.session.preloader.snapshotCache.snapshots - - return preloadedUrl in cache - }) - ) - } - - async "test preloads snapshot on page visit"() { - // contains `a[rel="preload"][href="http://localhost:9000/src/tests/fixtures/preloading.html"]` - await this.goToLocation("/src/tests/fixtures/hot_preloading.html") - - // contains `a[rel="preload"][href="http://localhost:9000/src/tests/fixtures/preloaded.html"]` - await this.clickSelector("#hot_preload_anchor") - await this.waitUntilSelector("#preload_anchor") - await this.nextBeat - - this.assert.ok( - await this.remote.execute(() => { - const preloadedUrl = "http://localhost:9000/src/tests/fixtures/preloaded.html" - const cache = window.Turbo.session.preloader.snapshotCache.snapshots - - return preloadedUrl in cache - }) - ) - } - - async "test navigates to preloaded snapshot from frame"() { - // contains `a[rel="preload"][href="http://localhost:9000/src/tests/fixtures/preloaded.html"]` - await this.goToLocation("/src/tests/fixtures/frame_preloading.html") - await this.waitUntilSelector("#frame_preload_anchor") - await this.nextBeat - - this.assert.ok( - await this.remote.execute(() => { - const preloadedUrl = "http://localhost:9000/src/tests/fixtures/preloaded.html" - const cache = window.Turbo.session.preloader.snapshotCache.snapshots - - return preloadedUrl in cache - }) - ) - } -} - -PreloaderTests.registerSuite() +import { test } from "@playwright/test" +import { assert } from "chai" +import { nextBeat } from "../helpers/page" + +test("test preloads snapshot on initial load", async ({ page }) => { + // contains `a[rel="preload"][href="http://localhost:9000/src/tests/fixtures/preloaded.html"]` + await page.goto("/src/tests/fixtures/preloading.html") + await nextBeat() + + assert.ok( + await page.evaluate(() => { + const preloadedUrl = "http://localhost:9000/src/tests/fixtures/preloaded.html" + const cache = window.Turbo.session.preloader.snapshotCache.snapshots + + return preloadedUrl in cache + }) + ) +}) + +test("test preloads snapshot on page visit", async ({ page }) => { + // contains `a[rel="preload"][href="http://localhost:9000/src/tests/fixtures/preloading.html"]` + await page.goto("/src/tests/fixtures/hot_preloading.html") + + // contains `a[rel="preload"][href="http://localhost:9000/src/tests/fixtures/preloaded.html"]` + await page.click("#hot_preload_anchor") + await page.waitForSelector("#preload_anchor") + await nextBeat() + + assert.ok( + await page.evaluate(() => { + const preloadedUrl = "http://localhost:9000/src/tests/fixtures/preloaded.html" + const cache = window.Turbo.session.preloader.snapshotCache.snapshots + + return preloadedUrl in cache + }) + ) +}) + +test("test navigates to preloaded snapshot from frame", async ({ page }) => { + // contains `a[rel="preload"][href="http://localhost:9000/src/tests/fixtures/preloaded.html"]` + await page.goto("/src/tests/fixtures/frame_preloading.html") + await page.waitForSelector("#frame_preload_anchor") + await nextBeat() + + assert.ok( + await page.evaluate(() => { + const preloadedUrl = "http://localhost:9000/src/tests/fixtures/preloaded.html" + const cache = window.Turbo.session.preloader.snapshotCache.snapshots + + return preloadedUrl in cache + }) + ) +}) diff --git a/src/tests/functional/rendering_tests.ts b/src/tests/functional/rendering_tests.ts index 2a55ddb97..f881abbb2 100644 --- a/src/tests/functional/rendering_tests.ts +++ b/src/tests/functional/rendering_tests.ts @@ -1,387 +1,363 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" -import { Element } from "@theintern/leadfoot" - -export class RenderingTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/rendering.html") - } - - async teardown() { - await this.remote.execute(() => localStorage.clear()) - } - - async "test triggers before-render and render events"() { - this.clickSelector("#same-origin-link") - const { newBody } = await this.nextEventNamed("turbo:before-render") - - const h1 = await this.querySelector("h1") - this.assert.equal(await h1.getVisibleText(), "One") - - await this.nextEventNamed("turbo:render") - this.assert(await newBody.equals(await this.body)) - } - - async "test triggers before-render and render events for error pages"() { - this.clickSelector("#nonexistent-link") - const { newBody } = await this.nextEventNamed("turbo:before-render") - - this.assert.equal(await newBody.getVisibleText(), "404 Not Found: /nonexistent") - - await this.nextEventNamed("turbo:render") - this.assert(await newBody.equals(await this.body)) - } - - async "test reloads when tracked elements change"() { - await this.remote.execute(() => - window.addEventListener("turbo:reload", (e: any) => { +import { JSHandle, Page, test } from "@playwright/test" +import { assert } from "chai" +import { + clearLocalStorage, + disposeAll, + isScrolledToTop, + nextBeat, + nextBody, + nextEventNamed, + pathname, + scrollToSelector, + selectorHasFocus, + sleep, + strictElementEquals, + textContent, + visitAction, +} from "../helpers/page" + +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/rendering.html") + await clearLocalStorage(page) +}) + +test("test triggers before-render and render events", async ({ page }) => { + await page.click("#same-origin-link") + const { newBody } = await nextEventNamed(page, "turbo:before-render") + + assert.equal(await page.textContent("h1"), "One") + + await nextEventNamed(page, "turbo:render") + assert.equal(await newBody, await page.evaluate(() => document.body.outerHTML)) +}) + +test("test triggers before-render and render events for error pages", async ({ page }) => { + await page.click("#nonexistent-link") + const { newBody } = await nextEventNamed(page, "turbo:before-render") + + assert.equal(await textContent(page, newBody), "404 Not Found: /nonexistent\n") + + await nextEventNamed(page, "turbo:render") + assert.equal(await newBody, await page.evaluate(() => document.body.outerHTML)) +}) + +test("test reloads when tracked elements change", async ({ page }) => { + await page.evaluate(() => + window.addEventListener( + "turbo:reload", + (e: any) => { localStorage.setItem("reloadReason", e.detail.reason) - }) + }, + { once: true } ) + ) - this.clickSelector("#tracked-asset-change-link") - await this.nextBody + await page.click("#tracked-asset-change-link") + await nextBody(page) - const reason = await this.remote.execute(() => localStorage.getItem("reloadReason")) + const reason = await page.evaluate(() => localStorage.getItem("reloadReason")) - this.assert.equal(await this.pathname, "/src/tests/fixtures/tracked_asset_change.html") - this.assert.equal(await this.visitAction, "load") - this.assert.equal(reason, "tracked_element_mismatch") - } + assert.equal(pathname(page.url()), "/src/tests/fixtures/tracked_asset_change.html") + assert.equal(await visitAction(page), "load") + assert.equal(reason, "tracked_element_mismatch") +}) - async "test wont reload when tracked elements has a nonce"() { - this.clickSelector("#tracked-nonce-tag-link") - await this.nextBody - this.assert.equal(await this.pathname, "/src/tests/fixtures/tracked_nonce_tag.html") - this.assert.equal(await this.visitAction, "advance") - } +test("test wont reload when tracked elements has a nonce", async ({ page }) => { + await page.click("#tracked-nonce-tag-link") + await nextBody(page) + + assert.equal(pathname(page.url()), "/src/tests/fixtures/tracked_nonce_tag.html") + assert.equal(await visitAction(page), "advance") +}) - async "test reloads when turbo-visit-control setting is reload"() { - await this.remote.execute(() => - window.addEventListener("turbo:reload", (e: any) => { +test("test reloads when turbo-visit-control setting is reload", async ({ page }) => { + await page.evaluate(() => + window.addEventListener( + "turbo:reload", + (e: any) => { localStorage.setItem("reloadReason", e.detail.reason) - }) + }, + { once: true } ) + ) - this.clickSelector("#visit-control-reload-link") - await this.nextBody + await page.click("#visit-control-reload-link") + await nextBody(page) - const reason = await this.remote.execute(() => localStorage.getItem("reloadReason")) + const reason = await page.evaluate(() => localStorage.getItem("reloadReason")) - this.assert.equal(await this.pathname, "/src/tests/fixtures/visit_control_reload.html") - this.assert.equal(await this.visitAction, "load") - this.assert.equal(reason, "turbo_visit_control_is_reload") - } + assert.equal(pathname(page.url()), "/src/tests/fixtures/visit_control_reload.html") + assert.equal(await visitAction(page), "load") + assert.equal(reason, "turbo_visit_control_is_reload") +}) - async "test maintains scroll position before visit when turbo-visit-control setting is reload"() { - await this.scrollToSelector("#below-the-fold-visit-control-reload-link") - this.assert.notOk(await this.isScrolledToTop(), "scrolled down") +test("test maintains scroll position before visit when turbo-visit-control setting is reload", async ({ page }) => { + await scrollToSelector(page, "#below-the-fold-visit-control-reload-link") + assert.notOk(await isScrolledToTop(page), "scrolled down") - await this.remote.execute(() => localStorage.setItem("scrolls", "false")) + await page.evaluate(() => localStorage.setItem("scrolls", "false")) - this.remote.execute(() => + page.evaluate(() => + addEventListener("click", () => { addEventListener("scroll", () => { localStorage.setItem("scrolls", "true") }) - ) - - this.clickSelector("#below-the-fold-visit-control-reload-link") - - await this.nextBody - - const scrolls = await this.remote.execute(() => localStorage.getItem("scrolls")) - this.assert.ok(scrolls === "false", "scroll position is preserved") - - this.assert.equal(await this.pathname, "/src/tests/fixtures/visit_control_reload.html") - this.assert.equal(await this.visitAction, "load") - } - - async "test accumulates asset elements in head"() { - const originalElements = await this.assetElements - - this.clickSelector("#additional-assets-link") - await this.nextBody - const newElements = await this.assetElements - this.assert.notDeepEqual(newElements, originalElements) - - this.goBack() - await this.nextBody - const finalElements = await this.assetElements - this.assert.deepEqual(finalElements, newElements) - } - - async "test replaces provisional elements in head"() { - const originalElements = await this.provisionalElements - this.assert(!(await this.hasSelector("meta[name=test]"))) - - this.clickSelector("#same-origin-link") - await this.nextBody - const newElements = await this.provisionalElements - this.assert.notDeepEqual(newElements, originalElements) - this.assert(await this.hasSelector("meta[name=test]")) - - this.goBack() - await this.nextBody - const finalElements = await this.provisionalElements - this.assert.notDeepEqual(finalElements, newElements) - this.assert(!(await this.hasSelector("meta[name=test]"))) - } - - async "test evaluates head stylesheet elements"() { - this.assert.equal(await this.isStylesheetEvaluated, false) - - this.clickSelector("#additional-assets-link") - await this.nextEventNamed("turbo:render") - this.assert.equal(await this.isStylesheetEvaluated, true) - } - - async "test does not evaluate head stylesheet elements inside noscript elements"() { - this.assert.equal(await this.isNoscriptStylesheetEvaluated, false) - - this.clickSelector("#additional-assets-link") - await this.nextEventNamed("turbo:render") - this.assert.equal(await this.isNoscriptStylesheetEvaluated, false) - } - - async "skip evaluates head script elements once"() { - this.assert.equal(await this.headScriptEvaluationCount, undefined) - - this.clickSelector("#head-script-link") - await this.nextEventNamed("turbo:render") - this.assert.equal(await this.headScriptEvaluationCount, 1) - - this.goBack() - await this.nextEventNamed("turbo:render") - this.assert.equal(await this.headScriptEvaluationCount, 1) - - this.clickSelector("#head-script-link") - await this.nextEventNamed("turbo:render") - this.assert.equal(await this.headScriptEvaluationCount, 1) - } - - async "test evaluates body script elements on each render"() { - this.assert.equal(await this.bodyScriptEvaluationCount, undefined) - - this.clickSelector("#body-script-link") - await this.nextEventNamed("turbo:render") - this.assert.equal(await this.bodyScriptEvaluationCount, 1) - - this.goBack() - await this.nextEventNamed("turbo:render") - this.assert.equal(await this.bodyScriptEvaluationCount, 1) - - this.clickSelector("#body-script-link") - await this.nextEventNamed("turbo:render") - this.assert.equal(await this.bodyScriptEvaluationCount, 2) - } - - async "test does not evaluate data-turbo-eval=false scripts"() { - this.clickSelector("#eval-false-script-link") - await this.nextEventNamed("turbo:render") - this.assert.equal(await this.bodyScriptEvaluationCount, undefined) - } - - async "test preserves permanent elements"() { - const permanentElement = await this.permanentElement - this.assert.equal(await permanentElement.getVisibleText(), "Rendering") - - this.clickSelector("#permanent-element-link") - await this.nextEventNamed("turbo:render") - this.assert(await permanentElement.equals(await this.permanentElement)) - this.assert.equal(await permanentElement.getVisibleText(), "Rendering") - - this.goBack() - await this.nextEventNamed("turbo:render") - this.assert(await permanentElement.equals(await this.permanentElement)) - } + }) + ) - async "test restores focus during page rendering when transposing the activeElement"() { - await this.clickSelector("#permanent-input") - await this.pressEnter() - await this.nextBody + page.click("#below-the-fold-visit-control-reload-link") - this.assert.ok(await this.selectorHasFocus("#permanent-input"), "restores focus after page loads") - } + await nextBody(page) - async "test restores focus during page rendering when transposing an ancestor of the activeElement"() { - await this.clickSelector("#permanent-descendant-input") - await this.pressEnter() - await this.nextBody + const scrolls = await page.evaluate(() => localStorage.getItem("scrolls")) + assert.equal(scrolls, "false", "scroll position is preserved") - this.assert.ok(await this.selectorHasFocus("#permanent-descendant-input"), "restores focus after page loads") - } + assert.equal(pathname(page.url()), "/src/tests/fixtures/visit_control_reload.html") + assert.equal(await visitAction(page), "load") +}) - async "test preserves permanent elements within turbo-frames"() { - let permanentElement = await this.querySelector("#permanent-in-frame") - this.assert.equal(await permanentElement.getVisibleText(), "Rendering") +test("test accumulates asset elements in head", async ({ page }) => { + const assetElements = () => page.$$('script, style, link[rel="stylesheet"]') + const originalElements = await assetElements() + + await page.click("#additional-assets-link") + await nextBody(page) + const newElements = await assetElements() + assert.notOk(await deepElementsEqual(page, newElements, originalElements)) - await this.clickSelector("#permanent-in-frame-element-link") - await this.nextBeat - permanentElement = await this.querySelector("#permanent-in-frame") - this.assert.equal(await permanentElement.getVisibleText(), "Rendering") - } + await page.goBack() + await nextBody(page) + const finalElements = await assetElements() + assert.ok(await deepElementsEqual(page, finalElements, newElements)) - async "test preserves permanent elements within turbo-frames rendered without layouts"() { - let permanentElement = await this.querySelector("#permanent-in-frame") - this.assert.equal(await permanentElement.getVisibleText(), "Rendering") + await disposeAll(...originalElements, ...newElements, ...finalElements) +}) + +test("test replaces provisional elements in head", async ({ page }) => { + const provisionalElements = () => page.$$('head :not(script), head :not(style), head :not(link[rel="stylesheet"])') + const originalElements = await provisionalElements() + assert.equal(await page.locator("meta[name=test]").count(), 0) - await this.clickSelector("#permanent-in-frame-without-layout-element-link") - await this.nextBeat - permanentElement = await this.querySelector("#permanent-in-frame") - this.assert.equal(await permanentElement.getVisibleText(), "Rendering") - } + await page.click("#same-origin-link") + await nextBody(page) + const newElements = await provisionalElements() + assert.notOk(await deepElementsEqual(page, newElements, originalElements)) + assert.equal(await page.locator("meta[name=test]").count(), 1) - async "test restores focus during turbo-frame rendering when transposing the activeElement"() { - await this.clickSelector("#permanent-input-in-frame") - await this.pressEnter() - await this.nextBeat + await page.goBack() + await nextBody(page) + const finalElements = await provisionalElements() + assert.notOk(await deepElementsEqual(page, finalElements, newElements)) + assert.equal(await page.locator("meta[name=test]").count(), 0) - this.assert.ok(await this.selectorHasFocus("#permanent-input-in-frame"), "restores focus after page loads") - } + await disposeAll(...originalElements, ...newElements, ...finalElements) +}) - async "test restores focus during turbo-frame rendering when transposing a descendant of the activeElement"() { - await this.clickSelector("#permanent-descendant-input-in-frame") - await this.pressEnter() - await this.nextBeat +test("test evaluates head stylesheet elements", async ({ page }) => { + assert.equal(await isStylesheetEvaluated(page), false) - this.assert.ok( - await this.selectorHasFocus("#permanent-descendant-input-in-frame"), - "restores focus after page loads" - ) - } + await page.click("#additional-assets-link") + await nextEventNamed(page, "turbo:render") + assert.equal(await isStylesheetEvaluated(page), true) +}) - async "test preserves permanent element video playback"() { - let videoElement = await this.querySelector("#permanent-video") - await this.clickSelector("#permanent-video-button") - await this.sleep(500) +test("test does not evaluate head stylesheet elements inside noscript elements", async ({ page }) => { + assert.equal(await isNoscriptStylesheetEvaluated(page), false) - const timeBeforeRender = await videoElement.getProperty("currentTime") - this.assert.notEqual(timeBeforeRender, 0, "playback has started") + await page.click("#additional-assets-link") + await nextEventNamed(page, "turbo:render") + assert.equal(await isNoscriptStylesheetEvaluated(page), false) +}) - await this.clickSelector("#permanent-element-link") - await this.nextBody - videoElement = await this.querySelector("#permanent-video") +test("skip evaluates head script elements once", async ({ page }) => { + assert.equal(await headScriptEvaluationCount(page), undefined) - const timeAfterRender = await videoElement.getProperty("currentTime") - this.assert.equal(timeAfterRender, timeBeforeRender, "element state is preserved") - } + await page.click("#head-script-link") + await nextEventNamed(page, "turbo:render") + assert.equal(await headScriptEvaluationCount(page), 1) - async "test before-cache event"() { - this.beforeCache((body) => (body.innerHTML = "Modified")) - this.clickSelector("#same-origin-link") - await this.nextBody - await this.goBack() - const body = await this.nextBody - this.assert(await body.getVisibleText(), "Modified") - } + await page.goBack() + await nextEventNamed(page, "turbo:render") + assert.equal(await headScriptEvaluationCount(page), 1) - async "test mutation record as before-cache notification"() { - this.modifyBodyAfterRemoval() - this.clickSelector("#same-origin-link") - await this.nextBody - await this.goBack() - const body = await this.nextBody - this.assert(await body.getVisibleText(), "Modified") - } + await page.click("#head-script-link") + await nextEventNamed(page, "turbo:render") + assert.equal(await headScriptEvaluationCount(page), 1) +}) - async "test error pages"() { - this.clickSelector("#nonexistent-link") - const body = await this.nextBody - this.assert.equal(await body.getVisibleText(), "404 Not Found: /nonexistent") - await this.goBack() - } +test("test evaluates body script elements on each render", async ({ page }) => { + assert.equal(await bodyScriptEvaluationCount(page), undefined) - get assetElements(): Promise { - return filter(this.headElements, isAssetElement) - } + await page.click("#body-script-link") + await nextEventNamed(page, "turbo:render") + assert.equal(await bodyScriptEvaluationCount(page), 1) - get provisionalElements(): Promise { - return filter(this.headElements, async (element) => !(await isAssetElement(element))) - } + await page.goBack() + await nextEventNamed(page, "turbo:render") + assert.equal(await bodyScriptEvaluationCount(page), 1) - get headElements(): Promise { - return this.evaluate(() => Array.from(document.head.children) as any[]) - } + await page.click("#body-script-link") + await nextEventNamed(page, "turbo:render") + assert.equal(await bodyScriptEvaluationCount(page), 2) +}) - get permanentElement(): Promise { - return this.querySelector("#permanent") - } +test("test does not evaluate data-turbo-eval=false scripts", async ({ page }) => { + await page.click("#eval-false-script-link") + await nextEventNamed(page, "turbo:render") + assert.equal(await bodyScriptEvaluationCount(page), undefined) +}) - get headScriptEvaluationCount(): Promise { - return this.evaluate(() => window.headScriptEvaluationCount) - } - - get bodyScriptEvaluationCount(): Promise { - return this.evaluate(() => window.bodyScriptEvaluationCount) - } - - get isStylesheetEvaluated(): Promise { - return this.evaluate( - () => getComputedStyle(document.body).getPropertyValue("--black-if-evaluated").trim() === "black" - ) - } - - get isNoscriptStylesheetEvaluated(): Promise { - return this.evaluate( - () => getComputedStyle(document.body).getPropertyValue("--black-if-noscript-evaluated").trim() === "black" - ) - } +test("test preserves permanent elements", async ({ page }) => { + const permanentElement = await page.locator("#permanent") + assert.equal(await permanentElement.textContent(), "Rendering") + + await page.click("#permanent-element-link") + await nextEventNamed(page, "turbo:render") + assert.ok(await strictElementEquals(permanentElement, await page.locator("#permanent"))) + assert.equal(await permanentElement!.textContent(), "Rendering") + + await page.goBack() + await nextEventNamed(page, "turbo:render") + assert.ok(await strictElementEquals(permanentElement, await page.locator("#permanent"))) +}) + +test("test restores focus during page rendering when transposing the activeElement", async ({ page }) => { + await page.press("#permanent-input", "Enter") + await nextBody(page) + + assert.ok(await selectorHasFocus(page, "#permanent-input"), "restores focus after page loads") +}) + +test("test restores focus during page rendering when transposing an ancestor of the activeElement", async ({ + page, +}) => { + await page.press("#permanent-descendant-input", "Enter") + await nextBody(page) + + assert.ok(await selectorHasFocus(page, "#permanent-descendant-input"), "restores focus after page loads") +}) + +test("test preserves permanent elements within turbo-frames", async ({ page }) => { + assert.equal(await page.textContent("#permanent-in-frame"), "Rendering") + + await page.click("#permanent-in-frame-element-link") + await nextBeat() + + assert.equal(await page.textContent("#permanent-in-frame"), "Rendering") +}) + +test("test preserves permanent elements within turbo-frames rendered without layouts", async ({ page }) => { + assert.equal(await page.textContent("#permanent-in-frame"), "Rendering") + + await page.click("#permanent-in-frame-without-layout-element-link") + await nextBeat() + + assert.equal(await page.textContent("#permanent-in-frame"), "Rendering") +}) + +test("test restores focus during turbo-frame rendering when transposing the activeElement", async ({ page }) => { + await page.press("#permanent-input-in-frame", "Enter") + await nextBeat() + + assert.ok(await selectorHasFocus(page, "#permanent-input-in-frame"), "restores focus after page loads") +}) + +test("test restores focus during turbo-frame rendering when transposing a descendant of the activeElement", async ({ + page, +}) => { + await page.press("#permanent-descendant-input-in-frame", "Enter") + await nextBeat() + + assert.ok(await selectorHasFocus(page, "#permanent-descendant-input-in-frame"), "restores focus after page loads") +}) + +test("test preserves permanent element video playback", async ({ page }) => { + const videoElement = await page.locator("#permanent-video") + await page.click("#permanent-video-button") + await sleep(500) + + const timeBeforeRender = await videoElement.evaluate((video: HTMLVideoElement) => video.currentTime) + assert.notEqual(timeBeforeRender, 0, "playback has started") + + await page.click("#permanent-element-link") + await nextBody(page) + + const timeAfterRender = await videoElement.evaluate((video: HTMLVideoElement) => video.currentTime) + assert.equal(timeAfterRender, timeBeforeRender, "element state is preserved") +}) + +test("test before-cache event", async ({ page }) => { + await page.evaluate(() => { + addEventListener("turbo:before-cache", () => (document.body.innerHTML = "Modified"), { once: true }) + }) + await page.click("#same-origin-link") + await nextBody(page) + await page.goBack() + + assert.equal(await page.textContent("body"), "Modified") +}) + +test("test mutation record as before-cache notification", async ({ page }) => { + await modifyBodyAfterRemoval(page) + await page.click("#same-origin-link") + await nextBody(page) + await page.goBack() + + assert.equal(await page.textContent("body"), "Modified") +}) + +test("test error pages", async ({ page }) => { + await page.click("#nonexistent-link") + await nextBody(page) + + assert.equal(await page.textContent("body"), "404 Not Found: /nonexistent\n") +}) + +function deepElementsEqual( + page: Page, + left: JSHandle[], + right: JSHandle[] +): Promise { + return page.evaluate( + ([left, right]) => left.length == right.length && left.every((element) => right.includes(element)), + [left, right] + ) +} - async modifyBodyBeforeCaching() { - return this.remote.execute(() => - addEventListener( - "turbo:before-cache", - function eventListener() { - removeEventListener("turbo:before-cache", eventListener, false) - document.body.innerHTML = "Modified" - }, - false - ) - ) - } +function headScriptEvaluationCount(page: Page): Promise { + return page.evaluate(() => window.headScriptEvaluationCount) +} - async beforeCache(callback: (body: HTMLElement) => void) { - return this.remote.execute( - (callback: (body: HTMLElement) => void) => { - addEventListener( - "turbo:before-cache", - function eventListener() { - removeEventListener("turbo:before-cache", eventListener, false) - callback(document.body) - }, - false - ) - }, - [callback] - ) - } +function bodyScriptEvaluationCount(page: Page): Promise { + return page.evaluate(() => window.bodyScriptEvaluationCount) +} - async modifyBodyAfterRemoval() { - return this.remote.execute(() => { - const { documentElement, body } = document - const observer = new MutationObserver((records) => { - for (const record of records) { - if (Array.from(record.removedNodes).indexOf(body) > -1) { - body.innerHTML = "Modified" - observer.disconnect() - break - } - } - }) - observer.observe(documentElement, { childList: true }) - }) - } +function isStylesheetEvaluated(page: Page): Promise { + return page.evaluate( + () => getComputedStyle(document.body).getPropertyValue("--black-if-evaluated").trim() === "black" + ) } -async function filter(promisedValues: Promise, predicate: (value: T) => Promise): Promise { - const values = await promisedValues - const matches = await Promise.all(values.map((value) => predicate(value))) - return matches.reduce((result, match, index) => result.concat(match ? values[index] : []), [] as T[]) +function isNoscriptStylesheetEvaluated(page: Page): Promise { + return page.evaluate( + () => getComputedStyle(document.body).getPropertyValue("--black-if-noscript-evaluated").trim() === "black" + ) } -async function isAssetElement(element: Element): Promise { - const tagName = await element.getTagName() - const relValue = await element.getAttribute("rel") - return tagName == "script" || tagName == "style" || (tagName == "link" && relValue == "stylesheet") +function modifyBodyAfterRemoval(page: Page) { + return page.evaluate(() => { + const { documentElement, body } = document + const observer = new MutationObserver((records) => { + for (const record of records) { + if (Array.from(record.removedNodes).indexOf(body) > -1) { + body.innerHTML = "Modified" + observer.disconnect() + break + } + } + }) + observer.observe(documentElement, { childList: true }) + }) } declare global { @@ -390,5 +366,3 @@ declare global { bodyScriptEvaluationCount?: number } } - -RenderingTests.registerSuite() diff --git a/src/tests/functional/scroll_restoration_tests.ts b/src/tests/functional/scroll_restoration_tests.ts index 9f10fe5f2..d8b82bb4c 100644 --- a/src/tests/functional/scroll_restoration_tests.ts +++ b/src/tests/functional/scroll_restoration_tests.ts @@ -1,33 +1,31 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" +import { nextBeat, scrollPosition, scrollToSelector } from "../helpers/page" -export class ScrollRestorationTests extends TurboDriveTestCase { - async "test landing on an anchor"() { - await this.goToLocation("/src/tests/fixtures/scroll_restoration.html#three") - await this.nextBody - const { y: yAfterLoading } = await this.scrollPosition - this.assert.notEqual(yAfterLoading, 0) - } +test("test landing on an anchor", async ({ page }) => { + await page.goto("/src/tests/fixtures/scroll_restoration.html#three") + await nextBeat() + const { y: yAfterLoading } = await scrollPosition(page) + assert.notEqual(yAfterLoading, 0) +}) - async "test reloading after scrolling"() { - await this.goToLocation("/src/tests/fixtures/scroll_restoration.html") - await this.scrollToSelector("#three") - const { y: yAfterScrolling } = await this.scrollPosition - this.assert.notEqual(yAfterScrolling, 0) +test("test reloading after scrolling", async ({ page }) => { + await page.goto("/src/tests/fixtures/scroll_restoration.html") + await scrollToSelector(page, "#three") + const { y: yAfterScrolling } = await scrollPosition(page) + assert.notEqual(yAfterScrolling, 0) - await this.reload() - const { y: yAfterReloading } = await this.scrollPosition - this.assert.notEqual(yAfterReloading, 0) - } + await page.reload() + const { y: yAfterReloading } = await scrollPosition(page) + assert.notEqual(yAfterReloading, 0) +}) - async "test returning from history"() { - await this.goToLocation("/src/tests/fixtures/scroll_restoration.html") - await this.scrollToSelector("#three") - await this.goToLocation("/src/tests/fixtures/bare.html") - await this.goBack() +test("test returning from history", async ({ page }) => { + await page.goto("/src/tests/fixtures/scroll_restoration.html") + await scrollToSelector(page, "#three") + await page.goto("/src/tests/fixtures/bare.html") + await page.goBack() - const { y: yAfterReturning } = await this.scrollPosition - this.assert.notEqual(yAfterReturning, 0) - } -} - -ScrollRestorationTests.registerSuite() + const { y: yAfterReturning } = await scrollPosition(page) + assert.notEqual(yAfterReturning, 0) +}) diff --git a/src/tests/functional/stream_tests.ts b/src/tests/functional/stream_tests.ts index 74acc3350..be1aa8e2d 100644 --- a/src/tests/functional/stream_tests.ts +++ b/src/tests/functional/stream_tests.ts @@ -1,68 +1,63 @@ -import { FunctionalTestCase } from "../helpers/functional_test_case" +import { test } from "@playwright/test" +import { assert } from "chai" +import { nextBeat } from "../helpers/page" -export class StreamTests extends FunctionalTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/stream.html") - } +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/stream.html") +}) - async "test receiving a stream message"() { - let element - const selector = "#messages div.message:last-child" +test("test receiving a stream message", async ({ page }) => { + const selector = "#messages div.message:last-child" - element = await this.querySelector(selector) - this.assert.equal(await element.getVisibleText(), "First") + assert.equal(await page.textContent(selector), "First") - await this.clickSelector("#create [type=submit]") - await this.nextBeat + await page.click("#create [type=submit]") + await nextBeat() - element = await this.querySelector(selector) - this.assert.equal(await element.getVisibleText(), "Hello world!") - } + assert.equal(await page.textContent(selector), "Hello world!") +}) - async "test receiving a stream message with css selector target"() { - let element - const selector = ".messages div.message:last-child" +test("test receiving a stream message with css selector target", async ({ page }) => { + let element + const selector = ".messages div.message:last-child" - element = await this.querySelectorAll(selector) - this.assert.equal(await element[0].getVisibleText(), "Second") - this.assert.equal(await element[1].getVisibleText(), "Third") + element = await page.locator(selector).allTextContents() + assert.equal(await element[0], "Second") + assert.equal(await element[1], "Third") - await this.clickSelector("#replace [type=submit]") - await this.nextBeat + await page.click("#replace [type=submit]") + await nextBeat() - element = await this.querySelectorAll(selector) - this.assert.equal(await element[0].getVisibleText(), "Hello CSS!") - this.assert.equal(await element[1].getVisibleText(), "Hello CSS!") - } + element = await page.locator(selector).allTextContents() + assert.equal(await element[0], "Hello CSS!") + assert.equal(await element[1], "Hello CSS!") +}) - async "test receiving a stream message asynchronously"() { - let messages = await this.querySelectorAll("#messages > *") +test("test receiving a stream message asynchronously", async ({ page }) => { + let messages = await page.locator("#messages > *").allTextContents() - this.assert.ok(messages[0]) - this.assert.notOk(messages[1], "receives streams when connected") - this.assert.notOk(messages[2], "receives streams when connected") + assert.ok(messages[0]) + assert.notOk(messages[1], "receives streams when connected") + assert.notOk(messages[2], "receives streams when connected") - await this.clickSelector("#async button") - await this.nextBeat + await page.click("#async button") + await nextBeat() - messages = await this.querySelectorAll("#messages > *") + messages = await page.locator("#messages > *").allTextContents() - this.assert.ok(messages[0]) - this.assert.ok(messages[1], "receives streams when connected") - this.assert.notOk(messages[2], "receives streams when connected") + assert.ok(messages[0]) + assert.ok(messages[1], "receives streams when connected") + assert.notOk(messages[2], "receives streams when connected") - await this.evaluate(() => document.getElementById("stream-source")?.remove()) - await this.nextBeat + await page.evaluate(() => document.getElementById("stream-source")?.remove()) + await nextBeat() - await this.clickSelector("#async button") - await this.nextBeat + await page.click("#async button") + await nextBeat() - messages = await this.querySelectorAll("#messages > *") + messages = await page.locator("#messages > *").allTextContents() - this.assert.ok(messages[0]) - this.assert.ok(messages[1], "receives streams when connected") - this.assert.notOk(messages[2], "does not receive streams when disconnected") - } -} - -StreamTests.registerSuite() + assert.ok(messages[0]) + assert.ok(messages[1], "receives streams when connected") + assert.notOk(messages[2], "does not receive streams when disconnected") +}) diff --git a/src/tests/functional/visit_tests.ts b/src/tests/functional/visit_tests.ts index 0b4da6f0f..b5603121d 100644 --- a/src/tests/functional/visit_tests.ts +++ b/src/tests/functional/visit_tests.ts @@ -1,153 +1,142 @@ -import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case" +import { Page, test } from "@playwright/test" +import { assert } from "chai" import { get } from "http" +import { nextBeat, nextEventNamed, readEventLogs, visitAction, willChangeBody } from "../helpers/page" -export class VisitTests extends TurboDriveTestCase { - async setup() { - await this.goToLocation("/src/tests/fixtures/visit.html") - } - - async "test programmatically visiting a same-origin location"() { - const urlBeforeVisit = await this.location - await this.visitLocation("/src/tests/fixtures/one.html") - - await this.nextBeat - - const urlAfterVisit = await this.location - this.assert.notEqual(urlBeforeVisit, urlAfterVisit) - this.assert.equal(await this.visitAction, "advance") - - const { url: urlFromBeforeVisitEvent } = await this.nextEventNamed("turbo:before-visit") - this.assert.equal(urlFromBeforeVisitEvent, urlAfterVisit) - - const { url: urlFromVisitEvent } = await this.nextEventNamed("turbo:visit") - this.assert.equal(urlFromVisitEvent, urlAfterVisit) - - const { timing } = await this.nextEventNamed("turbo:load") - this.assert.ok(timing) - } - - async "skip programmatically visiting a cross-origin location falls back to window.location"() { - const urlBeforeVisit = await this.location - await this.visitLocation("about:blank") - - const urlAfterVisit = await this.location - this.assert.notEqual(urlBeforeVisit, urlAfterVisit) - this.assert.equal(await this.visitAction, "load") - } - - async "test visiting a location served with a non-HTML content type"() { - const urlBeforeVisit = await this.location - await this.visitLocation("/src/tests/fixtures/svg.svg") - await this.nextBeat - - const url = await this.remote.getCurrentUrl() - const contentType = await contentTypeOfURL(url) - this.assert.equal(contentType, "image/svg+xml") - - const urlAfterVisit = await this.location - this.assert.notEqual(urlBeforeVisit, urlAfterVisit) - this.assert.equal(await this.visitAction, "load") - } - - async "test canceling a before-visit event prevents navigation"() { - this.cancelNextVisit() - const urlBeforeVisit = await this.location - - this.clickSelector("#same-origin-link") - await this.nextBeat - this.assert(!(await this.changedBody)) - - const urlAfterVisit = await this.location - this.assert.equal(urlAfterVisit, urlBeforeVisit) - } - - async "test navigation by history is not cancelable"() { - this.clickSelector("#same-origin-link") - await this.drainEventLog() - await this.nextBeat - - this.cancelNextVisit() - await this.goBack() - this.assert(await this.changedBody) - } - - async "test turbo:before-fetch-request event.detail"() { - await this.clickSelector("#same-origin-link") - const { url, fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request") - - this.assert.equal(fetchOptions.method, "GET") - this.assert.ok(url.toString().includes("/src/tests/fixtures/one.html")) - } - - async "test turbo:before-fetch-request event.detail encodes searchParams"() { - await this.clickSelector("#same-origin-link-search-params") - const { url } = await this.nextEventNamed("turbo:before-fetch-request") - - this.assert.ok(url.includes("/src/tests/fixtures/one.html?key=value")) - } - - async "test turbo:before-fetch-response open new site"() { - this.remote.execute(() => - addEventListener( - "turbo:before-fetch-response", - async function eventListener(event: any) { - removeEventListener("turbo:before-fetch-response", eventListener, false) - ;(window as any).fetchResponseResult = { - responseText: await event.detail.fetchResponse.responseText, - responseHTML: await event.detail.fetchResponse.responseHTML, - } - }, - false - ) - ) +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/visit.html") + await readEventLogs(page) +}) - await this.clickSelector("#sample-response") - await this.nextEventNamed("turbo:before-fetch-response") +test("test programmatically visiting a same-origin location", async ({ page }) => { + const urlBeforeVisit = page.url() + await visitLocation(page, "/src/tests/fixtures/one.html") - const fetchResponseResult = await this.evaluate(() => (window as any).fetchResponseResult) + await nextBeat() - this.assert.isTrue(fetchResponseResult.responseText.indexOf("An element with an ID") > -1) - this.assert.isTrue(fetchResponseResult.responseHTML.indexOf("An element with an ID") > -1) - } + const urlAfterVisit = page.url() + assert.notEqual(urlBeforeVisit, urlAfterVisit) + assert.equal(await visitAction(page), "advance") - async "test cache does not override response after redirect"() { - await this.remote.execute(() => { - const cachedElement = document.createElement("some-cached-element") - document.body.appendChild(cachedElement) - }) + const { url: urlFromBeforeVisitEvent } = await nextEventNamed(page, "turbo:before-visit") + assert.equal(urlFromBeforeVisitEvent, urlAfterVisit) - this.assert(await this.hasSelector("some-cached-element")) - this.clickSelector("#same-origin-link") - await this.nextBeat - this.clickSelector("#redirection-link") - await this.nextBeat // 301 redirect response - await this.nextBeat // 200 response - this.assert.notOk(await this.hasSelector("some-cached-element")) - } - - async visitLocation(location: string) { - this.remote.execute((location: string) => window.Turbo.visit(location), [location]) - } - - async cancelNextVisit() { - this.remote.execute(() => - addEventListener( - "turbo:before-visit", - function eventListener(event) { - removeEventListener("turbo:before-visit", eventListener, false) - event.preventDefault() - }, - false - ) - ) - } + const { url: urlFromVisitEvent } = await nextEventNamed(page, "turbo:visit") + assert.equal(urlFromVisitEvent, urlAfterVisit) + + const { timing } = await nextEventNamed(page, "turbo:load") + assert.ok(timing) +}) + +test("skip programmatically visiting a cross-origin location falls back to window.location", async ({ page }) => { + const urlBeforeVisit = page.url() + await visitLocation(page, "about:blank") + + const urlAfterVisit = page.url() + assert.notEqual(urlBeforeVisit, urlAfterVisit) + assert.equal(await visitAction(page), "load") +}) - async getDocumentElementAttribute(attributeName: string): Promise { - return await this.remote.execute( - (attributeName: string) => document.documentElement.getAttribute(attributeName), - [attributeName] +test("test visiting a location served with a non-HTML content type", async ({ page }) => { + const urlBeforeVisit = page.url() + await visitLocation(page, "/src/tests/fixtures/svg.svg") + await nextBeat() + + const url = page.url() + const contentType = await contentTypeOfURL(url) + assert.equal(contentType, "image/svg+xml") + + const urlAfterVisit = page.url() + assert.notEqual(urlBeforeVisit, urlAfterVisit) + assert.equal(await visitAction(page), "load") +}) + +test("test canceling a before-visit event prevents navigation", async ({ page }) => { + await cancelNextVisit(page) + const urlBeforeVisit = page.url() + + assert.notOk( + await willChangeBody(page, async () => { + await page.click("#same-origin-link") + await nextBeat() + }) + ) + + const urlAfterVisit = page.url() + assert.equal(urlAfterVisit, urlBeforeVisit) +}) + +test("test navigation by history is not cancelable", async ({ page }) => { + await page.click("#same-origin-link") + await nextEventNamed(page, "turbo:load") + + assert.equal(await page.textContent("h1"), "One") + + await cancelNextVisit(page) + await page.goBack() + await nextEventNamed(page, "turbo:load") + + assert.equal(await page.textContent("h1"), "Visit") +}) + +test("test turbo:before-fetch-request event.detail", async ({ page }) => { + await page.click("#same-origin-link") + const { url, fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") + + assert.equal(fetchOptions.method, "GET") + assert.ok(url.includes("/src/tests/fixtures/one.html")) +}) + +test("test turbo:before-fetch-request event.detail encodes searchParams", async ({ page }) => { + await page.click("#same-origin-link-search-params") + const { url } = await nextEventNamed(page, "turbo:before-fetch-request") + + assert.ok(url.includes("/src/tests/fixtures/one.html?key=value")) +}) + +test("test turbo:before-fetch-response open new site", async ({ page }) => { + page.evaluate(() => + addEventListener( + "turbo:before-fetch-response", + async function eventListener(event: any) { + removeEventListener("turbo:before-fetch-response", eventListener, false) + ;(window as any).fetchResponseResult = { + responseText: await event.detail.fetchResponse.responseText, + responseHTML: await event.detail.fetchResponse.responseHTML, + } + }, + false ) - } + ) + + await page.click("#sample-response") + await nextEventNamed(page, "turbo:before-fetch-response") + + const fetchResponseResult = await page.evaluate(() => (window as any).fetchResponseResult) + + assert.isTrue(fetchResponseResult.responseText.indexOf("An element with an ID") > -1) + assert.isTrue(fetchResponseResult.responseHTML.indexOf("An element with an ID") > -1) +}) + +test("test cache does not override response after redirect", async ({ page }) => { + await page.evaluate(() => { + const cachedElement = document.createElement("some-cached-element") + document.body.appendChild(cachedElement) + }) + + assert.equal(await page.locator("some-cached-element").count(), 1) + + await page.click("#same-origin-link") + await nextBeat() + await page.click("#redirection-link") + await nextBeat() // 301 redirect response + await nextBeat() // 200 response + + assert.equal(await page.locator("some-cached-element").count(), 0) +}) + +function cancelNextVisit(page: Page): Promise { + return page.evaluate(() => addEventListener("turbo:before-visit", (event) => event.preventDefault(), { once: true })) } function contentTypeOfURL(url: string): Promise { @@ -156,4 +145,6 @@ function contentTypeOfURL(url: string): Promise { }) } -VisitTests.registerSuite() +async function visitLocation(page: Page, location: string) { + return page.evaluate((location) => window.Turbo.visit(location), location) +} diff --git a/src/tests/helpers/functional_test_case.ts b/src/tests/helpers/functional_test_case.ts deleted file mode 100644 index bb93e165f..000000000 --- a/src/tests/helpers/functional_test_case.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { InternTestCase } from "./intern_test_case" -import { Element } from "@theintern/leadfoot" - -export class FunctionalTestCase extends InternTestCase { - get remote() { - return this.internTest.remote - } - - async goToLocation(location: string): Promise { - const processedLocation = location.match(/^\//) ? location.slice(1) : location - return this.remote.get(processedLocation) - } - - async goBack(): Promise { - return this.remote.goBack() - } - - async goForward(): Promise { - return this.remote.goForward() - } - - async reload(): Promise { - await this.evaluate(() => location.reload()) - return this.nextBeat - } - - async hasSelector(selector: string) { - return (await this.remote.findAllByCssSelector(selector)).length > 0 - } - - async selectorHasFocus(selector: string) { - const activeElement = await this.remote.getActiveElement() - - return activeElement.equals(await this.querySelector(selector)) - } - - async querySelector(selector: string) { - return this.remote.findByCssSelector(selector) - } - - async waitUntilSelector(selector: string): Promise { - return (async () => { - let hasSelector = false - do hasSelector = await this.hasSelector(selector) - while (!hasSelector) - })() - } - - async waitUntilNoSelector(selector: string): Promise { - return (async () => { - let hasSelector = true - do hasSelector = await this.hasSelector(selector) - while (hasSelector) - })() - } - - async querySelectorAll(selector: string) { - return this.remote.findAllByCssSelector(selector) - } - - async clickSelector(selector: string): Promise { - return (await this.remote.findByCssSelector(selector)).click() - } - - async scrollToSelector(selector: string): Promise { - const element = await this.remote.findByCssSelector(selector) - return this.evaluate((element) => element.scrollIntoView(), element) - } - - async pressTab(): Promise { - return this.remote.getActiveElement().then((activeElement) => activeElement.type("\uE004")) // TAB - } - - async pressEnter(): Promise { - return this.remote.getActiveElement().then((activeElement) => activeElement.type("\uE006")) // ENTER - } - - async outerHTMLForSelector(selector: string): Promise { - const element = await this.remote.findByCssSelector(selector) - return this.evaluate((element) => element.outerHTML, element) - } - - async innerHTMLForSelector(selector: string): Promise { - const element = await this.remote.findAllByCssSelector(selector) - return this.evaluate((element) => element.innerHTML, element) - } - - async attributeForSelector(selector: string, attributeName: string) { - const element = await this.querySelector(selector) - - return await element.getAttribute(attributeName) - } - - async propertyForSelector(selector: string, attributeName: string) { - const element = await this.querySelector(selector) - - return await element.getProperty(attributeName) - } - - get scrollPosition(): Promise<{ x: number; y: number }> { - return this.evaluate(() => ({ x: window.scrollX, y: window.scrollY })) - } - - async isScrolledToTop(): Promise { - const { y: pageY } = await this.scrollPosition - return pageY === 0 - } - - async isScrolledToSelector(selector: string): Promise { - const { y: pageY } = await this.scrollPosition - const { y: elementY } = await this.remote.findByCssSelector(selector).getPosition() - const offset = pageY - elementY - return Math.abs(offset) < 2 - } - - get nextBeat(): Promise { - return this.sleep(100) - } - - async sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)) - } - - async evaluate(callback: (...args: any[]) => T, ...args: any[]): Promise { - return await this.remote.execute(callback, args) - } - - get head(): Promise { - return this.evaluate(() => document.head as any) - } - - get body(): Promise { - return this.evaluate(() => document.body as any) - } - - get location(): Promise { - return this.evaluate(() => location.toString()) - } - - get origin(): Promise { - return this.evaluate(() => location.origin.toString()) - } - - get pathname(): Promise { - return this.evaluate(() => location.pathname) - } - - get search(): Promise { - return this.evaluate(() => location.search) - } - - get searchParams(): Promise { - return this.evaluate(() => location.search).then((search) => new URLSearchParams(search)) - } - - async getSearchParam(key: string): Promise { - return (await this.searchParams).get(key) || "" - } - - async getAllSearchParams(key: string): Promise { - return (await this.searchParams).getAll(key) || [] - } - - get hash(): Promise { - return this.evaluate(() => location.hash) - } - - async acceptAlert(): Promise { - return this.remote.acceptAlert() - } - - async dismissAlert(): Promise { - return this.remote.dismissAlert() - } - - async getAlertText(): Promise { - return this.remote.getAlertText() - } -} diff --git a/src/tests/helpers/page.ts b/src/tests/helpers/page.ts new file mode 100644 index 000000000..347e3eb80 --- /dev/null +++ b/src/tests/helpers/page.ts @@ -0,0 +1,241 @@ +import { JSHandle, Locator, Page } from "@playwright/test" + +type EventLog = [string, any, string | null] +type MutationLog = [string, string | null, string | null] + +export function attributeForSelector(page: Page, selector: string, attributeName: string): Promise { + return page.locator(selector).getAttribute(attributeName) +} + +export function clickWithoutScrolling(page: Page, selector: string, options = {}) { + const element = page.locator(selector, options) + + return element.evaluate((element) => element instanceof HTMLElement && element.click()) +} + +export function clearLocalStorage(page: Page): Promise { + return page.evaluate(() => localStorage.clear()) +} + +export function disposeAll(...handles: JSHandle[]): Promise { + return Promise.all(handles.map((handle) => handle.dispose())) +} + +export function getFromLocalStorage(page: Page, key: string) { + return page.evaluate((storageKey: string) => localStorage.getItem(storageKey), key) +} + +export function getSearchParam(url: string, key: string): string | null { + return searchParams(url).get(key) +} + +export function hash(url: string): string { + const { hash } = new URL(url) + + return hash +} + +export async function hasSelector(page: Page, selector: string): Promise { + return !!(await page.locator(selector).count()) +} + +export function innerHTMLForSelector(page: Page, selector: string): Promise { + return page.locator(selector).innerHTML() +} + +export async function isScrolledToSelector(page: Page, selector: string): Promise { + const boundingBox = await page + .locator(selector) + .evaluate((element) => (element instanceof HTMLElement ? { x: element.offsetLeft, y: element.offsetTop } : null)) + + if (boundingBox) { + const { y: pageY } = await scrollPosition(page) + const { y: elementY } = boundingBox + const offset = pageY - elementY + return Math.abs(offset) < 2 + } else { + return false + } +} + +export function nextBeat() { + return sleep(100) +} + +export function nextBody(_page: Page, timeout = 500) { + return sleep(timeout) +} + +export async function nextEventNamed(page: Page, eventName: string): Promise { + let record: EventLog | undefined + while (!record) { + const records = await readEventLogs(page, 1) + record = records.find(([name]) => name == eventName) + } + return record[1] +} + +export async function nextEventOnTarget(page: Page, elementId: string, eventName: string): Promise { + let record: EventLog | undefined + while (!record) { + const records = await readEventLogs(page, 1) + record = records.find(([name, _, id]) => name == eventName && id == elementId) + } + return record[1] +} + +export async function nextAttributeMutationNamed( + page: Page, + elementId: string, + attributeName: string +): Promise { + let record: MutationLog | undefined + while (!record) { + const records = await readMutationLogs(page, 1) + record = records.find(([name, id]) => name == attributeName && id == elementId) + } + const attributeValue = record[2] + return attributeValue +} + +export async function noNextEventNamed(page: Page, eventName: string): Promise { + const records = await readEventLogs(page, 1) + return !records.some(([name]) => name == eventName) +} + +export async function noNextEventOnTarget(page: Page, elementId: string, eventName: string): Promise { + const records = await readEventLogs(page, 1) + return !records.some(([name, _, target]) => name == eventName && target == elementId) +} + +export async function outerHTMLForSelector(page: Page, selector: string): Promise { + const element = await page.locator(selector) + return element.evaluate((element) => element.outerHTML) +} + +export function pathname(url: string): string { + const { pathname } = new URL(url) + + return pathname +} + +export function propertyForSelector(page: Page, selector: string, propertyName: string): Promise { + return page.locator(selector).evaluate((element, propertyName) => (element as any)[propertyName], propertyName) +} + +async function readArray(page: Page, identifier: string, length?: number): Promise { + return page.evaluate( + ({ identifier, length }) => { + const records = (window as any)[identifier] + if (records != null && typeof records.splice == "function") { + return records.splice(0, typeof length === "undefined" ? records.length : length) + } else { + return [] + } + }, + { identifier, length } + ) +} + +export function readEventLogs(page: Page, length?: number): Promise { + return readArray(page, "eventLogs", length) +} + +export function readMutationLogs(page: Page, length?: number): Promise { + return readArray(page, "mutationLogs", length) +} + +export function search(url: string): string { + const { search } = new URL(url) + + return search +} + +export function searchParams(url: string): URLSearchParams { + const { searchParams } = new URL(url) + + return searchParams +} + +export function selectorHasFocus(page: Page, selector: string): Promise { + return page.locator(selector).evaluate((element) => element === document.activeElement) +} + +export function setLocalStorageFromEvent(page: Page, eventName: string, storageKey: string, storageValue: string) { + return page.evaluate( + ({ eventName, storageKey, storageValue }) => { + addEventListener(eventName, () => localStorage.setItem(storageKey, storageValue)) + }, + { eventName, storageKey, storageValue } + ) +} + +export function scrollPosition(page: Page): Promise<{ x: number; y: number }> { + return page.evaluate(() => ({ x: window.scrollX, y: window.scrollY })) +} + +export async function isScrolledToTop(page: Page): Promise { + const { y: pageY } = await scrollPosition(page) + return pageY === 0 +} + +export function scrollToSelector(page: Page, selector: string): Promise { + return page.locator(selector).scrollIntoViewIfNeeded() +} + +export function sleep(timeout = 0): Promise { + return new Promise((resolve) => setTimeout(() => resolve(undefined), timeout)) +} + +export async function strictElementEquals(left: Locator, right: Locator): Promise { + return left.evaluate((left, right) => left === right, await right.elementHandle()) +} + +export function textContent(page: Page, html: string): Promise { + return page.evaluate((html) => { + const parser = new DOMParser() + const { documentElement } = parser.parseFromString(html, "text/html") + + return documentElement.textContent + }, html) +} + +export function visitAction(page: Page): Promise { + return page.evaluate(() => { + try { + return window.Turbo.navigator.currentVisit!.action + } catch (error) { + return "load" + } + }) +} + +export function waitForPathname(page: Page, pathname: string): Promise { + return page.waitForURL((url) => url.pathname == pathname) +} + +export function waitUntilSelector(page: Page, selector: string, state: "visible" | "attached" = "visible") { + return page.waitForSelector(selector, { state }) +} + +export function waitUntilNoSelector(page: Page, selector: string, state: "hidden" | "detached" = "hidden") { + return page.waitForSelector(selector, { state }) +} + +export async function willChangeBody(page: Page, callback: () => Promise): Promise { + const handles: JSHandle[] = [] + + try { + const originalBody = await page.evaluateHandle(() => document.body) + handles.push(originalBody) + + await callback() + + const latestBody = await page.evaluateHandle(() => document.body) + handles.push(latestBody) + + return page.evaluate(({ originalBody, latestBody }) => originalBody !== latestBody, { originalBody, latestBody }) + } finally { + disposeAll(...handles) + } +} diff --git a/src/tests/helpers/remote_channel.ts b/src/tests/helpers/remote_channel.ts deleted file mode 100644 index 22e4a17d7..000000000 --- a/src/tests/helpers/remote_channel.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Remote } from "intern/lib/executors/Node" - -export class RemoteChannel { - readonly remote: Remote - readonly identifier: string - private index = 0 - - constructor(remote: Remote, identifier: string) { - this.remote = remote - this.identifier = identifier - } - - async read(length?: number): Promise { - const records = (await this.newRecords).slice(0, length) - this.index += records.length - return records - } - - async drain(): Promise { - await this.read() - } - - private get newRecords() { - return this.remote.execute( - (identifier: string, index: number) => { - const records = (window as any)[identifier] - if (records != null && typeof records.slice == "function") { - return records.slice(index) - } else { - return [] - } - }, - [this.identifier, this.index] - ) - } -} diff --git a/src/tests/helpers/turbo_drive_test_case.ts b/src/tests/helpers/turbo_drive_test_case.ts deleted file mode 100644 index 830e463a6..000000000 --- a/src/tests/helpers/turbo_drive_test_case.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { FunctionalTestCase } from "./functional_test_case" -import { RemoteChannel } from "./remote_channel" -import { Element } from "@theintern/leadfoot" - -type EventLog = [string, any, string | null] -type MutationLog = [string, string | null, string | null] - -export class TurboDriveTestCase extends FunctionalTestCase { - eventLogChannel: RemoteChannel = new RemoteChannel(this.remote, "eventLogs") - mutationLogChannel: RemoteChannel = new RemoteChannel(this.remote, "mutationLogs") - lastBody?: Element - - async beforeTest() { - await this.clearLocalStorage() - await this.drainEventLog() - this.lastBody = await this.body - } - - get nextWindowHandle(): Promise { - return (async (nextHandle?: string) => { - do { - const handle = await this.remote.getCurrentWindowHandle() - const handles = await this.remote.getAllWindowHandles() - nextHandle = handles[handles.indexOf(handle) + 1] - } while (!nextHandle) - return nextHandle - })() - } - - async nextEventNamed(eventName: string): Promise { - let record: EventLog | undefined - while (!record) { - const records = await this.eventLogChannel.read(1) - record = records.find(([name]) => name == eventName) - } - return record[1] - } - - async noNextEventNamed(eventName: string): Promise { - const records = await this.eventLogChannel.read(1) - return !records.some(([name]) => name == eventName) - } - - async nextEventOnTarget(elementId: string, eventName: string): Promise { - let record: EventLog | undefined - while (!record) { - const records = await this.eventLogChannel.read(1) - record = records.find(([name, _, id]) => name == eventName && id == elementId) - } - return record[1] - } - - async nextAttributeMutationNamed(elementId: string, attributeName: string): Promise { - let record: MutationLog | undefined - while (!record) { - const records = await this.mutationLogChannel.read(1) - record = records.find(([name, id]) => name == attributeName && id == elementId) - } - const attributeValue = record[2] - return attributeValue - } - - async setLocalStorageFromEvent(event: string, key: string, value: string) { - return this.remote.execute( - (eventName: string, storageKey: string, storageValue: string) => { - addEventListener(eventName, () => localStorage.setItem(storageKey, storageValue)) - }, - [event, key, value] - ) - } - - getFromLocalStorage(key: string) { - return this.remote.execute((storageKey: string) => localStorage.getItem(storageKey), [key]) - } - - get nextBody(): Promise { - return (async () => { - let body - do body = await this.changedBody - while (!body) - return (this.lastBody = body) - })() - } - - get changedBody(): Promise { - return (async () => { - const body = await this.body - if (!this.lastBody || this.lastBody.elementId != body.elementId) { - return body - } - })() - } - - get visitAction(): Promise { - return this.evaluate(() => { - try { - return window.Turbo.navigator.currentVisit!.action - } catch (error) { - return "load" - } - }) - } - - drainEventLog() { - return this.eventLogChannel.drain() - } - - clearLocalStorage() { - this.remote.execute(() => localStorage.clear()) - } -} diff --git a/src/tests/runner.js b/src/tests/runner.js index 7672b9d60..0acc08560 100644 --- a/src/tests/runner.js +++ b/src/tests/runner.js @@ -33,14 +33,6 @@ if (args["--environment"]) { const firstArg = args["_"][0] if (firstArg == "serveOnly") { intern.configure({ serveOnly: true }) -} else { - const { spawnSync } = require("child_process") - const { status, stderr } = spawnSync("java", [ "-version" ]) - - if (status != 0) { - console.error(stderr.toString()) - process.exit(status) - } } intern.on("serverStart", server => { diff --git a/tsconfig.json b/tsconfig.json index dc422ad2d..6b133cf60 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,5 @@ "removeComments": true, "skipLibCheck": true, }, - "exclude": [ "dist", "src/tests/fixtures" ] + "exclude": [ "dist", "src/tests/fixtures", "playwright.config.ts" ] } diff --git a/yarn.lock b/yarn.lock index 270176402..efffcfc0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,95 +2,185 @@ # yarn lockfile v1 -"@babel/code-frame@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz" - integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== - dependencies: - "@babel/highlight" "^7.10.4" +"@ampproject/remapping@^2.1.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" + integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== + dependencies: + "@jridgewell/gen-mapping" "^0.1.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" + integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== + dependencies: + "@babel/highlight" "^7.16.7" + +"@babel/compat-data@^7.17.10": + version "7.18.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.5.tgz#acac0c839e317038c73137fbb6ef71a1d6238471" + integrity sha512-BxhE40PVCBxVEJsSBhB6UWyAuqJRxGsAw8BdHMJ3AKGydcwuWW4kOO3HmqBQAdcq/OP+/DlTVxLvsCzRTnZuGg== + +"@babel/core@^7.7.5": + version "7.18.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.5.tgz#c597fa680e58d571c28dda9827669c78cdd7f000" + integrity sha512-MGY8vg3DxMnctw0LdvSEojOsumc70g0t18gNyUdAZqB1Rpd1Bqo/svHGvt+UJ6JcGX+DIekGFDxxIWofBxLCnQ== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.18.2" + "@babel/helper-compilation-targets" "^7.18.2" + "@babel/helper-module-transforms" "^7.18.0" + "@babel/helpers" "^7.18.2" + "@babel/parser" "^7.18.5" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.18.5" + "@babel/types" "^7.18.4" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" -"@babel/generator@^7.12.5", "@babel/generator@^7.4.0": - version "7.12.5" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz" - integrity sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A== +"@babel/generator@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.2.tgz#33873d6f89b21efe2da63fe554460f3df1c5880d" + integrity sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw== dependencies: - "@babel/types" "^7.12.5" + "@babel/types" "^7.18.2" + "@jridgewell/gen-mapping" "^0.3.0" jsesc "^2.5.1" - source-map "^0.5.0" - -"@babel/helper-function-name@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz" - integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== - dependencies: - "@babel/helper-get-function-arity" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-get-function-arity@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz" - integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== - dependencies: - "@babel/types" "^7.10.4" -"@babel/helper-split-export-declaration@^7.11.0": - version "7.11.0" - resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz" - integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== - dependencies: - "@babel/types" "^7.11.0" - -"@babel/helper-validator-identifier@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz" - integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== - -"@babel/highlight@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz" - integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" +"@babel/helper-compilation-targets@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.2.tgz#67a85a10cbd5fc7f1457fec2e7f45441dc6c754b" + integrity sha512-s1jnPotJS9uQnzFtiZVBUxe67CuBa679oWFHpxYYnTpRL/1ffhyX44R9uYiXoa/pLXcY9H2moJta0iaanlk/rQ== + dependencies: + "@babel/compat-data" "^7.17.10" + "@babel/helper-validator-option" "^7.16.7" + browserslist "^4.20.2" + semver "^6.3.0" + +"@babel/helper-environment-visitor@^7.16.7", "@babel/helper-environment-visitor@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.2.tgz#8a6d2dedb53f6bf248e31b4baf38739ee4a637bd" + integrity sha512-14GQKWkX9oJzPiQQ7/J36FTXcD4kSp8egKjO9nINlSKiHITRA9q/R74qu8S9xlc/b/yjsJItQUeeh3xnGN0voQ== + +"@babel/helper-function-name@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz#136fcd54bc1da82fcb47565cf16fd8e444b1ff12" + integrity sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg== + dependencies: + "@babel/template" "^7.16.7" + "@babel/types" "^7.17.0" + +"@babel/helper-hoist-variables@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" + integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-module-imports@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" + integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-module-transforms@^7.18.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.0.tgz#baf05dec7a5875fb9235bd34ca18bad4e21221cd" + integrity sha512-kclUYSUBIjlvnzN2++K9f2qzYKFgjmnmjwL4zlmU5f8ZtzgWe8s0rUPSTGy2HmK4P8T52MQsS+HTQAgZd3dMEA== + dependencies: + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-simple-access" "^7.17.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.18.0" + "@babel/types" "^7.18.0" + +"@babel/helper-simple-access@^7.17.7": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.2.tgz#4dc473c2169ac3a1c9f4a51cfcd091d1c36fcff9" + integrity sha512-7LIrjYzndorDY88MycupkpQLKS1AFfsVRm2k/9PtKScSy5tZq0McZTj+DiMRynboZfIqOKvo03pmhTaUgiD6fQ== + dependencies: + "@babel/types" "^7.18.2" + +"@babel/helper-split-export-declaration@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b" + integrity sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== + +"@babel/helper-validator-option@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" + integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== + +"@babel/helpers@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.2.tgz#970d74f0deadc3f5a938bfa250738eb4ac889384" + integrity sha512-j+d+u5xT5utcQSzrh9p+PaJX94h++KN+ng9b9WEJq7pkUPAd61FGqhjuUEdfknb3E/uDBb7ruwEeKkIxNJPIrg== + dependencies: + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.18.2" + "@babel/types" "^7.18.2" + +"@babel/highlight@^7.16.7": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.17.12.tgz#257de56ee5afbd20451ac0a75686b6b404257351" + integrity sha512-7yykMVF3hfZY2jsHZEEgLc+3x4o1O+fYyULu11GynEUQNwB6lua+IIQn1FiJxNucd5UlyJryrwsOh8PL9Sn8Qg== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.12.7", "@babel/parser@^7.4.3": - version "7.12.7" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz" - integrity sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg== - -"@babel/template@^7.10.4", "@babel/template@^7.4.0": - version "7.12.7" - resolved "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz" - integrity sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/parser" "^7.12.7" - "@babel/types" "^7.12.7" - -"@babel/traverse@^7.4.3": - version "7.12.9" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.9.tgz" - integrity sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.12.5" - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/parser" "^7.12.7" - "@babel/types" "^7.12.7" +"@babel/parser@^7.16.7", "@babel/parser@^7.18.5": + version "7.18.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.5.tgz#337062363436a893a2d22faa60be5bb37091c83c" + integrity sha512-YZWVaglMiplo7v8f1oMQ5ZPQr0vn7HPeZXxXWsxXJRjGVrzUFn9OxFQl1sb5wzfootjA/yChhW84BV+383FSOw== + +"@babel/template@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" + integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/parser" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/traverse@^7.18.0", "@babel/traverse@^7.18.2", "@babel/traverse@^7.18.5": + version "7.18.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.5.tgz#94a8195ad9642801837988ab77f36e992d9a20cd" + integrity sha512-aKXj1KT66sBj0vVzk6rEeAO6Z9aiiQ68wfDgge3nHhA/my6xMM/7HGQUNumKZaoa2qUPQ5whJG9aAifsxUKfLA== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.18.2" + "@babel/helper-environment-visitor" "^7.18.2" + "@babel/helper-function-name" "^7.17.9" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.18.5" + "@babel/types" "^7.18.4" debug "^4.1.0" globals "^11.1.0" - lodash "^4.17.19" -"@babel/types@^7.10.4", "@babel/types@^7.11.0", "@babel/types@^7.12.5", "@babel/types@^7.12.7", "@babel/types@^7.4.0": - version "7.12.7" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz" - integrity sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ== +"@babel/types@^7.16.7", "@babel/types@^7.17.0", "@babel/types@^7.18.0", "@babel/types@^7.18.2", "@babel/types@^7.18.4": + version "7.18.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.4.tgz#27eae9b9fd18e9dccc3f9d6ad051336f307be354" + integrity sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw== dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - lodash "^4.17.19" + "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" "@eslint/eslintrc@^1.2.1": @@ -122,6 +212,51 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jridgewell/gen-mapping@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" + integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz#cf92a983c83466b8c0ce9124fadeaf09f7c66ea9" + integrity sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz#30cd49820a962aff48c8fffc5cd760151fca61fe" + integrity sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA== + +"@jridgewell/set-array@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.1.tgz#36a6acc93987adcf0ba50c66908bd0b70de8afea" + integrity sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.13" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c" + integrity sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w== + +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz#dcfe3e95f224c8fe97a87a5235defec999aa92ea" + integrity sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -143,6 +278,14 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@playwright/test@^1.22.2": + version "1.22.2" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.22.2.tgz#b848f25f8918140c2d0bae8e9227a40198f2dd4a" + integrity sha512-cCl96BEBGPtptFz7C2FOSN3PrTnJ3rPpENe+gYCMx4GNNDlN4tmo2D89y13feGKTMMAIVrXfSQ/UmaQKLy1XLA== + dependencies: + "@types/node" "*" + playwright-core "1.22.2" + "@rollup/plugin-node-resolve@13.1.3": version "13.1.3" resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.1.3.tgz" @@ -172,42 +315,62 @@ estree-walker "^1.0.1" picomatch "^2.2.2" -"@theintern/common@~0.2.3": - version "0.2.3" - resolved "https://registry.npmjs.org/@theintern/common/-/common-0.2.3.tgz" - integrity sha512-91kL3C6USiNfumAm5m07HjGdc40IJaNzEczhcdW5T8fjsHVNg8ttVSXCzy5C9BM57WLevVjR5eHUNjEl4foGMQ== +"@theintern/common@~0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@theintern/common/-/common-0.3.0.tgz#a8351b9ab815fa8b0d846e5373b626994a6e80ad" + integrity sha512-VKSyZGEyzmicJPvV5Gxeavm8Xbcr0cETAAqMapWZzA9Q85YHMG8VSrmPFlMrDQ524qE0IqQsTi0IlH8NIaN+eQ== dependencies: - axios "~0.19.0" - tslib "~1.9.3" + axios "~0.21.1" + tslib "~2.3.0" -"@theintern/digdug@~2.5.0": - version "2.5.0" - resolved "https://registry.npmjs.org/@theintern/digdug/-/digdug-2.5.0.tgz" - integrity sha512-g5mRt94GENnXxHgpccK9gjwyaK+61+fnF+njMnJGJQkxhjCjfShu9R3btt3/vSy5kkWxop83UN2/oAkiyqTKDw== +"@theintern/digdug@~2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@theintern/digdug/-/digdug-2.6.2.tgz#c03fab97cff3128108823d2eb2924bdf63a06a69" + integrity sha512-r9P7zkIp8L2LYKOUfcKl+KOHUTWrIZ9X6Efsb7Tn+OtiIv4oRlXorcoj/5vmrRLO5JF8jFj26HyeSWBNQA2uwg== dependencies: - "@theintern/common" "~0.2.3" - command-exists "~1.2.6" - decompress "~4.2.0" - tslib "~1.9.3" + "@theintern/common" "~0.3.0" + command-exists "~1.2.9" + decompress "~4.2.1" + tslib "~2.3.0" -"@theintern/leadfoot@~2.3.2": - version "2.3.2" - resolved "https://registry.npmjs.org/@theintern/leadfoot/-/leadfoot-2.3.2.tgz" - integrity sha512-NskDofysJMJad5uEYUc7Y3AlP3IdhY3t+H6XyiTDPur/p4pzVvKGGoCygE5FsN/K26i0XOmhNEIOwJtOMh47Hg== +"@theintern/leadfoot@~2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@theintern/leadfoot/-/leadfoot-2.4.1.tgz#4f8f69d968503e5b8488c17d39e35be2cae4d10f" + integrity sha512-WnmmMlSROXQc6sGJdQCcSXYbrRAni2HMmjjr2qtvXtLNCi7ZG6O/H7rJ+1fNdJckjE3kwF+Ag3Bh1WR7GkfG0Q== dependencies: - "@theintern/common" "~0.2.3" - jszip "~3.2.1" - tslib "~1.9.3" + "@theintern/common" "~0.3.0" + jszip "~3.7.1" + tslib "~2.3.0" + +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.1": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== "@types/babel-types@*": - version "7.0.9" - resolved "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.9.tgz" - integrity sha512-qZLoYeXSTgQuK1h7QQS16hqLGdmqtRmN8w/rl3Au/l5x/zkHx+a4VHrHyBsi1I1vtK2oBHxSzKIu0R5p6spdOA== + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.11.tgz#263b113fa396fac4373188d73225297fb86f19a9" + integrity sha512-pkPtJUUY+Vwv6B1inAz55rQvivClHJxc9aVEPPmaq2cbyeMLCiDpbKpcKyX4LAwpNGi+SHBv0tHv6+0gXv0P2A== -"@types/benchmark@1.0.31": - version "1.0.31" - resolved "https://registry.npmjs.org/@types/benchmark/-/benchmark-1.0.31.tgz" - integrity sha512-F6fVNOkGEkSdo/19yWYOwVKGvzbTeWkR/XQYBKtGBQ9oGRjBN9f/L4aJI4sDcVPJO58Y1CJZN8va9V2BhrZapA== +"@types/benchmark@~2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@types/benchmark/-/benchmark-2.1.1.tgz#d763df29717d93aa333eb11f421ef383a5df5673" + integrity sha512-XmdNOarpSSxnb3DE2rRFOFsEyoqXLUL+7H8nSGS25vs+JS0018bd+cW5Ma9vdlkPmoTHSQ6e8EUFMFMxeE4l+g== "@types/body-parser@*": version "1.19.0" @@ -217,15 +380,15 @@ "@types/connect" "*" "@types/node" "*" -"@types/chai@4.1.7": - version "4.1.7" - resolved "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz" - integrity sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA== +"@types/chai@~4.2.20": + version "4.2.22" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.22.tgz#47020d7e4cf19194d43b5202f35f75bd2ad35ce7" + integrity sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ== -"@types/charm@1.0.1": - version "1.0.1" - resolved "https://registry.npmjs.org/@types/charm/-/charm-1.0.1.tgz" - integrity sha512-F9OalGhk60p/DnACfa1SWtmVTMni0+w9t/qfb5Bu7CsurkEjZFN7Z+ii/VGmYpaViPz7o3tBahRQae9O7skFlQ== +"@types/charm@~1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/charm/-/charm-1.0.3.tgz#1dc44bcbf0a90ef4b6826094fb324796d229d502" + integrity sha512-FpNoSOkloETr+ZJ0RsZpB+a/tqJkniIN+9Enn6uPIbhiNptOWtZzV7FkaqxTRjvvlHeUKMR331Wj9tOmqG10TA== dependencies: "@types/node" "*" @@ -241,11 +404,6 @@ resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/events@*": - version "3.0.0" - resolved "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz" - integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== - "@types/express-serve-static-core@*": version "4.17.14" resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.14.tgz" @@ -255,7 +413,16 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@*", "@types/express@~4.17.0": +"@types/express-serve-static-core@^4.17.18": + version "4.17.29" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz#2a1795ea8e9e9c91b4a4bbe475034b20c1ec711c" + integrity sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@*": version "4.17.9" resolved "https://registry.npmjs.org/@types/express/-/express-4.17.9.tgz" integrity sha512-SDzEIZInC4sivGIFY4Sz1GG6J9UObPwCInYJjko2jzOf/Imx/dlpume6Xxwj1ORL82tBbmN4cPDIDkLbWHk9hw== @@ -265,41 +432,50 @@ "@types/qs" "*" "@types/serve-static" "*" -"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@~2.0.1": - version "2.0.3" - resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz" - integrity sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw== +"@types/express@~4.17.13": + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" + integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.18" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@~2.0.3": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== -"@types/istanbul-lib-instrument@~1.7.3": +"@types/istanbul-lib-instrument@~1.7.4": version "1.7.4" - resolved "https://registry.npmjs.org/@types/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.4.tgz" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.4.tgz#474503169db59ada532dd863885c67b217ab67f1" integrity sha512-1i1VVkU2KrpZCmti+t5J/zBb2KLKxHgU1EYL+0QtnDnVyZ59aSKcpnG6J0I6BZGDON566YzPNIlNfk7m+9l1JA== dependencies: "@types/babel-types" "*" "@types/istanbul-lib-coverage" "*" source-map "^0.6.1" -"@types/istanbul-lib-report@*", "@types/istanbul-lib-report@~1.1.1": - version "1.1.1" - resolved "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz" - integrity sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg== +"@types/istanbul-lib-report@*", "@types/istanbul-lib-report@~3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== dependencies: "@types/istanbul-lib-coverage" "*" -"@types/istanbul-lib-source-maps@~1.2.2": - version "1.2.2" - resolved "https://registry.npmjs.org/@types/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.2.tgz" - integrity sha512-41eeNQ3Du3++LV0Hdz7m0UbeYMnShlJ7CkUOVy3tBeFwc0BE7chBs2Vqdx7xOzXBo2iRQfyiWBmqIZTbau3q+A== +"@types/istanbul-lib-source-maps@~4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#8acb1f6230bf9d732e9fc30590e5ccaabbefec7b" + integrity sha512-WH6e5naLXI3vB2Px3whNeYxzDgm6S6sk3Ht8e3/BiWwEnzZi72wja3bWzWwcgbFTFp8hBLB7NT2p3lNJgxCxvA== dependencies: "@types/istanbul-lib-coverage" "*" source-map "^0.6.1" -"@types/istanbul-reports@~1.1.1": - version "1.1.2" - resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz" - integrity sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw== +"@types/istanbul-reports@~3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== dependencies: - "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" "@types/json-schema@^7.0.9": @@ -349,12 +525,11 @@ "@types/mime" "*" "@types/node" "*" -"@types/ws@6.0.1": - version "6.0.1" - resolved "https://registry.npmjs.org/@types/ws/-/ws-6.0.1.tgz" - integrity sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q== +"@types/ws@7.4.6": + version "7.4.6" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.6.tgz#c4320845e43d45a7129bb32905e28781c71c1fff" + integrity sha512-ijZ1vzRawI7QoWnTNL8KpHixd2b2XVb9I9HAqI3triPsh1EC0xH0Eg6w2O3TKbDCgiNNlJqfrof6j4T2I+l9vw== dependencies: - "@types/events" "*" "@types/node" "*" "@typescript-eslint/eslint-plugin@^5.20.0": @@ -437,13 +612,13 @@ "@typescript-eslint/types" "5.20.0" eslint-visitor-keys "^3.0.0" -accepts@~1.3.7: - version "1.3.7" - resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" + mime-types "~2.1.34" + negotiator "0.6.3" acorn-jsx@^5.3.1: version "5.3.2" @@ -472,7 +647,7 @@ ansi-regex@^5.0.1: ansi-styles@^3.2.1: version "3.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" @@ -489,16 +664,16 @@ append-field@^1.0.0: resolved "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz" integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY= -append-transform@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz" - integrity sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw== +append-transform@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-2.0.0.tgz#99d9d29c7b38391e6f428d28ce136551f0b77e12" + integrity sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg== dependencies: - default-require-extensions "^2.0.0" + default-require-extensions "^3.0.0" arg@^4.1.0: version "4.1.3" - resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== arg@^5.0.1: @@ -513,8 +688,8 @@ argparse@^2.0.1: array-flatten@1.1.1: version "1.1.1" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== array-union@^2.1.0: version "2.1.0" @@ -523,20 +698,15 @@ array-union@^2.1.0: assertion-error@^1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== -async-limiter@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== - -axios@~0.19.0: - version "0.19.2" - resolved "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz" - integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== +axios@~0.21.1: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== dependencies: - follow-redirects "1.5.10" + follow-redirects "^1.14.0" balanced-match@^1.0.0: version "1.0.0" @@ -545,40 +715,40 @@ balanced-match@^1.0.0: base64-js@^1.3.1: version "1.5.1" - resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== benchmark@~2.1.4: version "2.1.4" - resolved "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz" - integrity sha1-CfPeMckWQl1JjMLuVloOvzwqVik= + resolved "https://registry.yarnpkg.com/benchmark/-/benchmark-2.1.4.tgz#09f3de31c916425d498cc2ee565a0ebf3c2a5629" + integrity sha512-l9MlfN4M1K/H2fbhfMy3B7vJd6AGKJVQn2h6Sg/Yx+KckoUA7ewS5Vv6TjSq18ooE1kS9hhAlQRH3AkXIh/aOQ== dependencies: lodash "^4.17.4" platform "^1.3.3" bl@^1.0.0: version "1.2.3" - resolved "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7" integrity sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww== dependencies: readable-stream "^2.3.5" safe-buffer "^5.1.1" -body-parser@1.19.0, body-parser@~1.19.0: - version "1.19.0" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz" - integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== +body-parser@1.19.2, body-parser@~1.19.0: + version "1.19.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.2.tgz#4714ccd9c157d44797b8b5607d72c0b89952f26e" + integrity sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw== dependencies: - bytes "3.1.0" + bytes "3.1.2" content-type "~1.0.4" debug "2.6.9" depd "~1.1.2" - http-errors "1.7.2" + http-errors "1.8.1" iconv-lite "0.4.24" on-finished "~2.3.0" - qs "6.7.0" - raw-body "2.4.0" - type-is "~1.6.17" + qs "6.9.7" + raw-body "2.4.3" + type-is "~1.6.18" brace-expansion@^1.1.7: version "1.1.11" @@ -595,14 +765,25 @@ braces@^3.0.2: dependencies: fill-range "^7.0.1" +browserslist@^4.20.2: + version "4.20.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.4.tgz#98096c9042af689ee1e0271333dbc564b8ce4477" + integrity sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw== + dependencies: + caniuse-lite "^1.0.30001349" + electron-to-chromium "^1.4.147" + escalade "^3.1.1" + node-releases "^2.0.5" + picocolors "^1.0.0" + buffer-alloc-unsafe@^1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== buffer-alloc@^1.2.0: version "1.2.0" - resolved "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== dependencies: buffer-alloc-unsafe "^1.1.0" @@ -610,13 +791,13 @@ buffer-alloc@^1.2.0: buffer-crc32@~0.2.3: version "0.2.13" - resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz" - integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== buffer-fill@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz" - integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" + integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ== buffer-from@^1.0.0: version "1.1.1" @@ -625,7 +806,7 @@ buffer-from@^1.0.0: buffer@^5.2.1: version "5.7.1" - resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== dependencies: base64-js "^1.3.1" @@ -644,31 +825,37 @@ busboy@^0.2.11: dicer "0.2.5" readable-stream "1.1.x" -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -chai@~4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz" - integrity sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw== +caniuse-lite@^1.0.30001349: + version "1.0.30001357" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001357.tgz#dec7fc4158ef6ad24690d0eec7b91f32b8cb1b5d" + integrity sha512-b+KbWHdHePp+ZpNj+RDHFChZmuN+J5EvuQUlee9jOQIUAdhv9uvAZeEtUeLAknXbkiu1uxjQ9NLp1ie894CuWg== + +chai@~4.3.4: + version "4.3.6" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.6.tgz#ffe4ba2d9fa9d6680cc0b370adae709ec9011e9c" + integrity sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q== dependencies: assertion-error "^1.1.0" check-error "^1.0.2" deep-eql "^3.0.1" get-func-name "^2.0.0" - pathval "^1.1.0" + loupe "^2.3.1" + pathval "^1.1.1" type-detect "^4.0.5" chalk@^2.0.0: version "2.4.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== dependencies: ansi-styles "^3.2.1" @@ -685,19 +872,19 @@ chalk@^4.0.0: charm@~1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/charm/-/charm-1.0.2.tgz" - integrity sha1-it02cVOm2aWBMxBSxAkJkdqZXjU= + resolved "https://registry.yarnpkg.com/charm/-/charm-1.0.2.tgz#8add367153a6d9a581331052c4090991da995e35" + integrity sha512-wqW3VdPnlSWT4eRiYX+hcs+C6ViBPUWk1qTCd+37qw9kEm/a5n2qcyQDMBWvSYKN/ctqZzeXNQaeBjOetJJUkw== dependencies: inherits "^2.0.1" check-error@^1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz" - integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" + integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA== color-convert@^1.9.0: version "1.9.3" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== dependencies: color-name "1.1.3" @@ -711,22 +898,22 @@ color-convert@^2.0.1: color-name@1.1.3: version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -command-exists@~1.2.6: +command-exists@~1.2.9: version "1.2.9" - resolved "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz" + resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69" integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w== commander@^2.8.1: version "2.20.3" - resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== concat-map@0.0.1: @@ -746,36 +933,48 @@ concat-stream@^1.5.2: concurrent@~0.3.2: version "0.3.2" - resolved "https://registry.npmjs.org/concurrent/-/concurrent-0.3.2.tgz" - integrity sha1-DqoAEaFXmMVjURKPIiR/biMX9Q4= + resolved "https://registry.yarnpkg.com/concurrent/-/concurrent-0.3.2.tgz#0eaa0011a15798c56351128f22247f6e2317f50e" + integrity sha512-KoUIH3pHceLMOeviiAnOzdQ8630lNclszDv8IGXx2Gn+5xXZroLqSWWzisweX//X7LyYOCKy10398bb0ksjvsA== -content-disposition@0.5.3: - version "0.5.3" - resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== dependencies: - safe-buffer "5.1.2" + safe-buffer "5.2.1" content-type@~1.0.4: version "1.0.4" - resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +convert-source-map@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + cookie-signature@1.0.6: version "1.0.6" - resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" - integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz" - integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +cookie@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -787,35 +986,28 @@ cross-spawn@^7.0.2: debug@2.6.9: version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@=3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== +debug@^4.1.0, debug@^4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: - ms "2.0.0" + ms "2.1.2" -debug@^4.1.0, debug@^4.1.1: +debug@^4.1.1: version "4.3.1" resolved "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz" integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== dependencies: ms "2.1.2" -debug@^4.3.2: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: version "4.1.1" - resolved "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz" + resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1" integrity sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ== dependencies: file-type "^5.2.0" @@ -824,7 +1016,7 @@ decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: decompress-tarbz2@^4.0.0: version "4.1.1" - resolved "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz" + resolved "https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b" integrity sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A== dependencies: decompress-tar "^4.1.0" @@ -835,7 +1027,7 @@ decompress-tarbz2@^4.0.0: decompress-targz@^4.0.0: version "4.1.1" - resolved "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz" + resolved "https://registry.yarnpkg.com/decompress-targz/-/decompress-targz-4.1.1.tgz#c09bc35c4d11f3de09f2d2da53e9de23e7ce1eee" integrity sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w== dependencies: decompress-tar "^4.1.1" @@ -844,17 +1036,17 @@ decompress-targz@^4.0.0: decompress-unzip@^4.0.1: version "4.0.1" - resolved "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz" - integrity sha1-3qrM39FK6vhVePczroIQ+bSEj2k= + resolved "https://registry.yarnpkg.com/decompress-unzip/-/decompress-unzip-4.0.1.tgz#deaaccdfd14aeaf85578f733ae8210f9b4848f69" + integrity sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw== dependencies: file-type "^3.8.0" get-stream "^2.2.0" pify "^2.3.0" yauzl "^2.4.2" -decompress@~4.2.0: +decompress@~4.2.1: version "4.2.1" - resolved "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz" + resolved "https://registry.yarnpkg.com/decompress/-/decompress-4.2.1.tgz#007f55cc6a62c055afa37c07eb6a4ee1b773f118" integrity sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ== dependencies: decompress-tar "^4.0.0" @@ -868,7 +1060,7 @@ decompress@~4.2.0: deep-eql@^3.0.1: version "3.0.1" - resolved "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw== dependencies: type-detect "^4.0.0" @@ -883,22 +1075,22 @@ deepmerge@^4.2.2: resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== -default-require-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz" - integrity sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc= +default-require-extensions@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-3.0.0.tgz#e03f93aac9b2b6443fc52e5e4a37b3ad9ad8df96" + integrity sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg== dependencies: - strip-bom "^3.0.0" + strip-bom "^4.0.0" depd@~1.1.2: version "1.1.2" - resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== destroy@~1.0.4: version "1.0.4" - resolved "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg== dicer@0.2.5: version "0.2.5" @@ -908,11 +1100,16 @@ dicer@0.2.5: readable-stream "1.1.x" streamsearch "0.1.2" -diff@^4.0.1, diff@~4.0.1: +diff@^4.0.1: version "4.0.2" - resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +diff@~5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -932,27 +1129,37 @@ ee-first@1.1.1: resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= +electron-to-chromium@^1.4.147: + version "1.4.161" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.161.tgz#49cb5b35385bfee6cc439d0a04fbba7a7a7f08a1" + integrity sha512-sTjBRhqh6wFodzZtc5Iu8/R95OkwaPNn7tj/TaDU5nu/5EFiQDtADGAXdR4tJcTEHlYfJpHqigzJqHvPgehP8A== + encodeurl@~1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" - integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== end-of-stream@^1.0.0: version "1.4.4" - resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + escape-html@~1.0.3: version "1.0.3" - resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" - integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== escape-string-regexp@^1.0.5: version "1.0.5" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== escape-string-regexp@^4.0.0: version "4.0.0" @@ -1090,20 +1297,20 @@ esutils@^2.0.2: etag@~1.8.1: version "1.8.1" - resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" - integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== express@~4.17.1: - version "4.17.1" - resolved "https://registry.npmjs.org/express/-/express-4.17.1.tgz" - integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + version "4.17.3" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1" + integrity sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg== dependencies: - accepts "~1.3.7" + accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.19.0" - content-disposition "0.5.3" + body-parser "1.19.2" + content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.4.0" + cookie "0.4.2" cookie-signature "1.0.6" debug "2.6.9" depd "~1.1.2" @@ -1117,13 +1324,13 @@ express@~4.17.1: on-finished "~2.3.0" parseurl "~1.3.3" path-to-regexp "0.1.7" - proxy-addr "~2.0.5" - qs "6.7.0" + proxy-addr "~2.0.7" + qs "6.9.7" range-parser "~1.2.1" - safe-buffer "5.1.2" - send "0.17.1" - serve-static "1.14.1" - setprototypeof "1.1.1" + safe-buffer "5.2.1" + send "0.17.2" + serve-static "1.14.2" + setprototypeof "1.2.0" statuses "~1.5.0" type-is "~1.6.18" utils-merge "1.0.1" @@ -1169,8 +1376,8 @@ fastq@^1.6.0: fd-slicer@~1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz" - integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== dependencies: pend "~1.2.0" @@ -1183,17 +1390,17 @@ file-entry-cache@^6.0.1: file-type@^3.8.0: version "3.9.0" - resolved "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz" - integrity sha1-JXoHg4TR24CHvESdEH1SpSZyuek= + resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" + integrity sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA== file-type@^5.2.0: version "5.2.0" - resolved "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz" - integrity sha1-LdvqfHP/42No365J3DOMBYwritY= + resolved "https://registry.yarnpkg.com/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6" + integrity sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ== file-type@^6.1.0: version "6.2.0" - resolved "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919" integrity sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg== fill-range@^7.0.1: @@ -1205,7 +1412,7 @@ fill-range@^7.0.1: finalhandler@~1.1.2: version "1.1.2" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== dependencies: debug "2.6.9" @@ -1229,26 +1436,24 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== -follow-redirects@1.5.10: - version "1.5.10" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz" - integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== - dependencies: - debug "=3.1.0" +follow-redirects@^1.14.0: + version "1.15.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" + integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== fresh@0.5.2: version "0.5.2" - resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" - integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== fs-constants@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== fs.realpath@^1.0.0: @@ -1271,15 +1476,20 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + get-func-name@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz" - integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" + integrity sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig== get-stream@^2.2.0: version "2.3.1" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz" - integrity sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4= + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de" + integrity sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA== dependencies: object-assign "^4.0.1" pinkie-promise "^2.0.0" @@ -1298,7 +1508,7 @@ glob-parent@^6.0.1: dependencies: is-glob "^4.0.3" -glob@^7.1.3, glob@~7.1.4: +glob@^7.1.3: version "7.1.6" resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -1310,9 +1520,21 @@ glob@^7.1.3, glob@~7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" +glob@~7.1.7: + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + globals@^11.1.0: version "11.12.0" - resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.6.0, globals@^13.9.0: @@ -1335,25 +1557,14 @@ globby@^11.0.4: slash "^3.0.0" graceful-fs@^4.1.10: - version "4.2.4" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== - -handlebars@~4.5.3: - version "4.5.3" - resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz" - integrity sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA== - dependencies: - neo-async "^2.6.0" - optimist "^0.6.1" - source-map "^0.6.1" - optionalDependencies: - uglify-js "^3.1.4" + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== has-flag@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== has-flag@^4.0.0: version "4.0.0" @@ -1369,41 +1580,30 @@ has@^1.0.3: html-escaper@^2.0.0: version "2.0.2" - resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -http-errors@1.7.2: - version "1.7.2" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz" - integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-errors@~1.7.2: - version "1.7.3" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== +http-errors@1.8.1, http-errors@~1.8.0: + version "1.8.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" + integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== dependencies: depd "~1.1.2" inherits "2.0.4" - setprototypeof "1.1.1" + setprototypeof "1.2.0" statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" + toidentifier "1.0.1" iconv-lite@0.4.24: version "0.4.24" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" ieee754@^1.1.13: version "1.2.1" - resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== ignore@^5.1.8, ignore@^5.2.0: @@ -1413,8 +1613,8 @@ ignore@^5.1.8, ignore@^5.2.0: immediate@~3.0.5: version "3.0.6" - resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz" - integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" @@ -1442,59 +1642,53 @@ inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, i resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - intern@^4.9.0: - version "4.9.0" - resolved "https://registry.npmjs.org/intern/-/intern-4.9.0.tgz" - integrity sha512-YxXmvizFf41tY1vjYjgnYknI2eDFHEhvtW8YrF3jwHTdBtDL0vcVBIIozmPBf28Ib7WsSLnzh5auqhzBkSBSRw== - dependencies: - "@theintern/common" "~0.2.3" - "@theintern/digdug" "~2.5.0" - "@theintern/leadfoot" "~2.3.2" - "@types/benchmark" "1.0.31" - "@types/chai" "4.1.7" - "@types/charm" "1.0.1" - "@types/express" "~4.17.0" - "@types/istanbul-lib-coverage" "~2.0.1" - "@types/istanbul-lib-instrument" "~1.7.3" - "@types/istanbul-lib-report" "~1.1.1" - "@types/istanbul-lib-source-maps" "~1.2.2" - "@types/istanbul-reports" "~1.1.1" - "@types/ws" "6.0.1" + version "4.10.1" + resolved "https://registry.yarnpkg.com/intern/-/intern-4.10.1.tgz#4dfba51d70d8c4eaf795f1006b5aeb4d6bef1747" + integrity sha512-GyUmdpdKGoEu1hRMNYeldPF11lFZlC1Pbq28ImzEY+7OHRDinMU9c8jwGxY7eAaUe15oy0Y7cocdjC/mzUuOng== + dependencies: + "@theintern/common" "~0.3.0" + "@theintern/digdug" "~2.6.2" + "@theintern/leadfoot" "~2.4.1" + "@types/benchmark" "~2.1.1" + "@types/chai" "~4.2.20" + "@types/charm" "~1.0.2" + "@types/express" "~4.17.13" + "@types/istanbul-lib-coverage" "~2.0.3" + "@types/istanbul-lib-instrument" "~1.7.4" + "@types/istanbul-lib-report" "~3.0.0" + "@types/istanbul-lib-source-maps" "~4.0.1" + "@types/istanbul-reports" "~3.0.1" + "@types/ws" "7.4.6" benchmark "~2.1.4" body-parser "~1.19.0" - chai "~4.2.0" + chai "~4.3.4" charm "~1.0.2" concurrent "~0.3.2" - diff "~4.0.1" + diff "~5.0.0" express "~4.17.1" - glob "~7.1.4" - handlebars "~4.5.3" - http-errors "~1.7.2" - istanbul-lib-coverage "~2.0.5" - istanbul-lib-hook "~2.0.7" - istanbul-lib-instrument "~3.3.0" - istanbul-lib-report "~2.0.8" - istanbul-lib-source-maps "~3.0.6" - istanbul-reports "~2.2.6" + glob "~7.1.7" + http-errors "~1.8.0" + istanbul-lib-coverage "~3.0.0" + istanbul-lib-hook "~3.0.0" + istanbul-lib-instrument "~4.0.3" + istanbul-lib-report "~3.0.0" + istanbul-lib-source-maps "~4.0.0" + istanbul-reports "~3.0.2" lodash "~4.17.15" - mime-types "~2.1.24" + mime-types "~2.1.31" minimatch "~3.0.4" - platform "~1.3.5" - resolve "~1.11.1" - shell-quote "~1.6.1" + platform "~1.3.6" + resolve "~1.20.0" + shell-quote "~1.7.2" source-map "~0.6.1" - ts-node "^8.2.0" - tslib "~1.9.3" - ws "~7.0.0" + ts-node "~10.0.0" + tslib "~2.3.0" + ws "~7.5.2" ipaddr.js@1.9.1: version "1.9.1" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== is-core-module@^2.1.0: @@ -1504,6 +1698,13 @@ is-core-module@^2.1.0: dependencies: has "^1.0.3" +is-core-module@^2.2.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" + integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== + dependencies: + has "^1.0.3" + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -1523,8 +1724,8 @@ is-module@^1.0.0: is-natural-number@^4.0.1: version "4.0.1" - resolved "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz" - integrity sha1-q5124dtM7VHjXeDHLr7PCfc0zeg= + resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" + integrity sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ== is-number@^7.0.0: version "7.0.0" @@ -1533,8 +1734,8 @@ is-number@^7.0.0: is-stream@^1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== isarray@0.0.1: version "0.0.1" @@ -1551,61 +1752,62 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= -istanbul-lib-coverage@^2.0.5, istanbul-lib-coverage@~2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz" - integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== +istanbul-lib-coverage@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== -istanbul-lib-hook@~2.0.7: - version "2.0.7" - resolved "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz" - integrity sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA== +istanbul-lib-coverage@~3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.2.tgz#36786d4d82aad2ea5911007e255e2da6b5f80d86" + integrity sha512-o5+eTUYzCJ11/+JhW5/FUCdfsdoYVdQ/8I/OveE2XsjehYn5DdeSnNQAbjYaO8gQ6hvGTN6GM6ddQqpTVG5j8g== + +istanbul-lib-hook@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz#8f84c9434888cc6b1d0a9d7092a76d239ebf0cc6" + integrity sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ== dependencies: - append-transform "^1.0.0" + append-transform "^2.0.0" -istanbul-lib-instrument@~3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz" - integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA== - dependencies: - "@babel/generator" "^7.4.0" - "@babel/parser" "^7.4.3" - "@babel/template" "^7.4.0" - "@babel/traverse" "^7.4.3" - "@babel/types" "^7.4.0" - istanbul-lib-coverage "^2.0.5" - semver "^6.0.0" +istanbul-lib-instrument@~4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" + integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== + dependencies: + "@babel/core" "^7.7.5" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.0.0" + semver "^6.3.0" -istanbul-lib-report@~2.0.8: - version "2.0.8" - resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz" - integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ== +istanbul-lib-report@^3.0.0, istanbul-lib-report@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== dependencies: - istanbul-lib-coverage "^2.0.5" - make-dir "^2.1.0" - supports-color "^6.1.0" + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" -istanbul-lib-source-maps@~3.0.6: - version "3.0.6" - resolved "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz" - integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== +istanbul-lib-source-maps@~4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== dependencies: debug "^4.1.1" - istanbul-lib-coverage "^2.0.5" - make-dir "^2.1.0" - rimraf "^2.6.3" + istanbul-lib-coverage "^3.0.0" source-map "^0.6.1" -istanbul-reports@~2.2.6: - version "2.2.7" - resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz" - integrity sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg== +istanbul-reports@~3.0.2: + version "3.0.5" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.5.tgz#a2580107e71279ea6d661ddede929ffc6d693384" + integrity sha512-5+19PlhnGabNWB7kOFnuxT8H3T/iIyQzIbQMxXsURmmvKg86P2sbkrGOT77VnHw0Qr0gc2XzRaRfMZYYbSQCJQ== dependencies: html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" js-tokens@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^4.1.0: @@ -1617,7 +1819,7 @@ js-yaml@^4.1.0: jsesc@^2.5.1: version "2.5.2" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== json-schema-traverse@^0.4.1: @@ -1630,10 +1832,15 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= -jszip@~3.2.1: - version "3.2.2" - resolved "https://registry.npmjs.org/jszip/-/jszip-3.2.2.tgz" - integrity sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA== +json5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + +jszip@~3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.7.1.tgz#bd63401221c15625a1228c556ca8a68da6fda3d9" + integrity sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg== dependencies: lie "~3.3.0" pako "~1.0.2" @@ -1650,7 +1857,7 @@ levn@^0.4.1: lie@~3.3.0: version "3.3.0" - resolved "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== dependencies: immediate "~3.0.5" @@ -1660,11 +1867,18 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.19, lodash@^4.17.4, lodash@~4.17.15: +lodash@^4.17.4, lodash@~4.17.15: version "4.17.21" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +loupe@^2.3.1: + version "2.3.4" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.4.tgz#7e0b9bffc76f148f9be769cb1321d3dcf3cb25f3" + integrity sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ== + dependencies: + get-func-name "^2.0.0" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -1674,22 +1888,21 @@ lru-cache@^6.0.0: make-dir@^1.0.0: version "1.3.0" - resolved "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== dependencies: pify "^3.0.0" -make-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== +make-dir@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== dependencies: - pify "^4.0.1" - semver "^5.6.0" + semver "^6.0.0" make-error@^1.1.1: version "1.3.6" - resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== media-typer@0.3.0: @@ -1699,8 +1912,8 @@ media-typer@0.3.0: merge-descriptors@1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" @@ -1709,8 +1922,8 @@ merge2@^1.3.0, merge2@^1.4.1: methods@~1.1.2: version "1.1.2" - resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== micromatch@^4.0.4: version "4.0.5" @@ -1725,6 +1938,11 @@ mime-db@1.44.0: resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + mime-types@~2.1.24: version "2.1.27" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz" @@ -1732,28 +1950,37 @@ mime-types@~2.1.24: dependencies: mime-db "1.44.0" +mime-types@~2.1.31, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mime@1.6.0: version "1.6.0" - resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -minimatch@^3.0.4, minimatch@~3.0.4: +minimatch@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== dependencies: brace-expansion "^1.1.7" +minimatch@~3.0.4: + version "3.0.8" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" + integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q== + dependencies: + brace-expansion "^1.1.7" + minimist@^1.2.5: version "1.2.5" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz" - integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= - mkdirp@^0.5.1: version "0.5.5" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz" @@ -1763,19 +1990,19 @@ mkdirp@^0.5.1: ms@2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + multer@^1.4.2: version "1.4.2" resolved "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz" @@ -1795,25 +2022,25 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -negotiator@0.6.2: - version "0.6.2" - resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== -neo-async@^2.6.0: - version "2.6.2" - resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +node-releases@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.5.tgz#280ed5bc3eba0d96ce44897d8aee478bfb3d9666" + integrity sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q== object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" - resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== on-finished@^2.3.0, on-finished@~2.3.0: version "2.3.0" - resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== dependencies: ee-first "1.1.1" @@ -1824,14 +2051,6 @@ once@^1.3.0, once@^1.4.0: dependencies: wrappy "1" -optimist@^0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz" - integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - optionator@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" @@ -1846,7 +2065,7 @@ optionator@^0.9.1: pako@~1.0.2: version "1.0.11" - resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== parent-module@^1.0.0: @@ -1858,7 +2077,7 @@ parent-module@^1.0.0: parseurl@~1.3.3: version "1.3.3" - resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== path-is-absolute@^1.0.0: @@ -1878,23 +2097,28 @@ path-parse@^1.0.6: path-to-regexp@0.1.7: version "0.1.7" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pathval@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz" - integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA= +pathval@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" + integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== pend@~1.2.0: version "1.2.0" - resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz" - integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== picomatch@^2.2.2: version "2.2.2" @@ -1908,36 +2132,36 @@ picomatch@^2.3.1: pify@^2.3.0: version "2.3.0" - resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== pify@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== pinkie-promise@^2.0.0: version "2.0.1" - resolved "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw== dependencies: pinkie "^2.0.0" pinkie@^2.0.0: version "2.0.4" - resolved "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== -platform@^1.3.3, platform@~1.3.5: +platform@^1.3.3, platform@~1.3.6: version "1.3.6" - resolved "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz" + resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== +playwright-core@1.22.2: + version "1.22.2" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.22.2.tgz#ed2963d79d71c2a18d5a6fd25b60b9f0a344661a" + integrity sha512-w/hc/Ld0RM4pmsNeE6aL/fPNWw8BWit2tg+TfqJ3+p59c6s3B6C8mXvXrIPmfQEobkcFDc+4KirNzOQ+uBSP1Q== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -1960,12 +2184,12 @@ process-nextick-args@~2.0.0: resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -proxy-addr@~2.0.5: - version "2.0.6" - resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz" - integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== dependencies: - forwarded "~0.1.2" + forwarded "0.2.0" ipaddr.js "1.9.1" punycode@^2.1.0: @@ -1973,10 +2197,10 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -qs@6.7.0: - version "6.7.0" - resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz" - integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@6.9.7: + version "6.9.7" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe" + integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== queue-microtask@^1.2.2: version "1.2.3" @@ -1985,16 +2209,16 @@ queue-microtask@^1.2.2: range-parser@~1.2.1: version "1.2.1" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.4.0: - version "2.4.0" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz" - integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== +raw-body@2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.3.tgz#8f80305d11c2a0a545c2d9d89d7a0286fcead43c" + integrity sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g== dependencies: - bytes "3.1.0" - http-errors "1.7.2" + bytes "3.1.2" + http-errors "1.8.1" iconv-lite "0.4.24" unpipe "1.0.0" @@ -2010,7 +2234,7 @@ readable-stream@1.1.x: readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@~2.3.6: version "2.3.7" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== dependencies: core-util-is "~1.0.0" @@ -2039,11 +2263,12 @@ resolve@^1.17.0, resolve@^1.19.0: is-core-module "^2.1.0" path-parse "^1.0.6" -resolve@~1.11.1: - version "1.11.1" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz" - integrity sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw== +resolve@~1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== dependencies: + is-core-module "^2.2.0" path-parse "^1.0.6" reusify@^1.0.4: @@ -2051,13 +2276,6 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -2079,31 +2297,31 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@5.1.2, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.2.1, safe-buffer@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== "safer-buffer@>= 2.1.2 < 3": version "2.1.2" - resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== seek-bzip@^1.0.5: version "1.0.6" - resolved "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz" + resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.6.tgz#35c4171f55a680916b52a07859ecf3b5857f21c4" integrity sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ== dependencies: commander "^2.8.1" -semver@^5.6.0: - version "5.7.1" - resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0: +semver@^6.0.0, semver@^6.3.0: version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== semver@^7.3.5: @@ -2113,10 +2331,10 @@ semver@^7.3.5: dependencies: lru-cache "^6.0.0" -send@0.17.1: - version "0.17.1" - resolved "https://registry.npmjs.org/send/-/send-0.17.1.tgz" - integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== +send@0.17.2: + version "0.17.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820" + integrity sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww== dependencies: debug "2.6.9" depd "~1.1.2" @@ -2125,32 +2343,32 @@ send@0.17.1: escape-html "~1.0.3" etag "~1.8.1" fresh "0.5.2" - http-errors "~1.7.2" + http-errors "1.8.1" mime "1.6.0" - ms "2.1.1" + ms "2.1.3" on-finished "~2.3.0" range-parser "~1.2.1" statuses "~1.5.0" -serve-static@1.14.1: - version "1.14.1" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz" - integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== +serve-static@1.14.2: + version "1.14.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.2.tgz#722d6294b1d62626d41b43a013ece4598d292bfa" + integrity sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ== dependencies: encodeurl "~1.0.2" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.17.1" + send "0.17.2" set-immediate-shim@~1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz" - integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E= + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + integrity sha512-Li5AOqrZWCVA2n5kryzEmqai6bKSIvpz5oUJHPVj6+dsbD3X1ixtsY5tEnsaNpH3pFAHmG8eIHUrtEtohrg+UQ== -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== shebang-command@^2.0.0: version "2.0.0" @@ -2164,10 +2382,10 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@~1.6.1: - version "1.6.3" - resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.3.tgz" - integrity sha512-KvITSOPOP542Mv4lS5Cx6/qgya20Hyk+JJUdfRfikzyV6iKPszdz5TrssURXRghmi6Z9y9gATRvxJ69zD7wydQ== +shell-quote@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" + integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== slash@^3.0.0: version "3.0.0" @@ -2175,27 +2393,22 @@ slash@^3.0.0: integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== source-map-support@^0.5.17: - version "0.5.19" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" -source-map@^0.5.0: - version "0.5.7" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== streamsearch@0.1.2: version "0.1.2" @@ -2221,14 +2434,14 @@ strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== strip-dirs@^2.0.0: version "2.1.0" - resolved "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5" integrity sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g== dependencies: is-natural-number "^4.0.1" @@ -2240,18 +2453,11 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: supports-color@^5.3.0: version "5.5.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== dependencies: has-flag "^3.0.0" -supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -2261,7 +2467,7 @@ supports-color@^7.1.0: tar-stream@^1.5.2: version "1.6.2" - resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A== dependencies: bl "^1.0.0" @@ -2279,18 +2485,18 @@ text-table@^0.2.0: through@^2.3.8: version "2.3.8" - resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== to-buffer@^1.1.1: version "1.1.1" - resolved "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg== to-fast-properties@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" - integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== to-regex-range@^5.0.1: version "5.0.1" @@ -2299,17 +2505,22 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - -ts-node@^8.2.0: - version "8.10.2" - resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz" - integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== - dependencies: +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +ts-node@~10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.0.0.tgz#05f10b9a716b0b624129ad44f0ea05dac84ba3be" + integrity sha512-ROWeOIUvfFbPZkoDis0L/55Fk+6gFQNZwwKPLinacRl6tsxstTF1DbAcLKkovwnpKMVvOMHP1TIbnwXwtLg1gg== + dependencies: + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.1" arg "^4.1.0" + create-require "^1.1.0" diff "^4.0.1" make-error "^1.1.1" source-map-support "^0.5.17" @@ -2325,10 +2536,10 @@ tslib@^2.0.3: resolved "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz" integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== -tslib@~1.9.3: - version "1.9.3" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz" - integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== +tslib@~2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" + integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== tsutils@^3.21.0: version "3.21.0" @@ -2346,7 +2557,7 @@ type-check@^0.4.0, type-check@~0.4.0: type-detect@^4.0.0, type-detect@^4.0.5: version "4.0.8" - resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== type-fest@^0.20.2: @@ -2354,9 +2565,9 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18: +type-is@^1.6.4, type-is@~1.6.18: version "1.6.18" - resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== dependencies: media-typer "0.3.0" @@ -2372,14 +2583,9 @@ typescript@^4.6.3: resolved "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz" integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw== -uglify-js@^3.1.4: - version "3.12.1" - resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.12.1.tgz" - integrity sha512-o8lHP20KjIiQe5b/67Rh68xEGRrc2SRsCuuoYclXXoC74AfSRGblU1HKzJWH3HxPZ+Ort85fWHpSX7KwBUC9CQ== - unbzip2-stream@^1.0.9: version "1.4.3" - resolved "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== dependencies: buffer "^5.2.1" @@ -2387,8 +2593,8 @@ unbzip2-stream@^1.0.9: unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== uri-js@^4.2.2: version "4.4.1" @@ -2404,8 +2610,8 @@ util-deprecate@~1.0.1: utils-merge@1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" - integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== v8-compile-cache@^2.0.3: version "2.3.0" @@ -2414,8 +2620,8 @@ v8-compile-cache@^2.0.3: vary@~1.1.2: version "1.1.2" - resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== which@^2.0.1: version "2.0.2" @@ -2429,22 +2635,15 @@ word-wrap@^1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" - integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= - wrappy@1: version "1.0.2" resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -ws@~7.0.0: - version "7.0.1" - resolved "https://registry.npmjs.org/ws/-/ws-7.0.1.tgz" - integrity sha512-ILHfMbuqLJvnSgYXLgy4kMntroJpe8hT41dOVWM8bxRuw6TK4mgMp9VJUNsZTEc5Bh+Mbs0DJT4M0N+wBG9l9A== - dependencies: - async-limiter "^1.0.0" +ws@~7.5.2: + version "7.5.8" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.8.tgz#ac2729881ab9e7cbaf8787fe3469a48c5c7f636a" + integrity sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw== xtend@^4.0.0: version "4.0.2" @@ -2458,13 +2657,13 @@ yallist@^4.0.0: yauzl@^2.4.2: version "2.10.0" - resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz" - integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== dependencies: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" yn@3.1.1: version "3.1.1" - resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== From d7430853cf3b46c33b4e1fe4b850266c19aea41a Mon Sep 17 00:00:00 2001 From: Manuel Puyol Date: Fri, 15 Jul 2022 18:30:47 -0500 Subject: [PATCH 11/14] Add original click event to 'turbo:click' details (#611) Co-authored-by: David Heinemeier Hansson --- src/core/session.ts | 12 ++++++------ src/observers/link_click_observer.ts | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/core/session.ts b/src/core/session.ts index a54231640..727019ba5 100644 --- a/src/core/session.ts +++ b/src/core/session.ts @@ -150,11 +150,11 @@ export class Session // Link click observer delegate - willFollowLinkToLocation(link: Element, location: URL) { + willFollowLinkToLocation(link: Element, location: URL, event: MouseEvent) { return ( this.elementDriveEnabled(link) && locationIsVisitable(location, this.snapshot.rootLocation) && - this.applicationAllowsFollowingLinkToLocation(link, location) + this.applicationAllowsFollowingLinkToLocation(link, location, event) ) } @@ -300,8 +300,8 @@ export class Session // Application events - applicationAllowsFollowingLinkToLocation(link: Element, location: URL) { - const event = this.notifyApplicationAfterClickingLinkToLocation(link, location) + applicationAllowsFollowingLinkToLocation(link: Element, location: URL, ev: MouseEvent) { + const event = this.notifyApplicationAfterClickingLinkToLocation(link, location, ev) return !event.defaultPrevented } @@ -310,10 +310,10 @@ export class Session return !event.defaultPrevented } - notifyApplicationAfterClickingLinkToLocation(link: Element, location: URL) { + notifyApplicationAfterClickingLinkToLocation(link: Element, location: URL, event: MouseEvent) { return dispatch("turbo:click", { target: link, - detail: { url: location.href }, + detail: { url: location.href, originalEvent: event }, cancelable: true, }) } diff --git a/src/observers/link_click_observer.ts b/src/observers/link_click_observer.ts index f0b0282ed..442f2495e 100644 --- a/src/observers/link_click_observer.ts +++ b/src/observers/link_click_observer.ts @@ -1,7 +1,7 @@ import { expandURL } from "../core/url" export interface LinkClickObserverDelegate { - willFollowLinkToLocation(link: Element, location: URL): boolean + willFollowLinkToLocation(link: Element, location: URL, event: MouseEvent): boolean followedLinkToLocation(link: Element, location: URL): void } @@ -38,7 +38,7 @@ export class LinkClickObserver { const link = this.findLinkFromClickTarget(target) if (link) { const location = this.getLocationForLink(link) - if (this.delegate.willFollowLinkToLocation(link, location)) { + if (this.delegate.willFollowLinkToLocation(link, location, event)) { event.preventDefault() this.delegate.followedLinkToLocation(link, location) } From 706e614221b21e3854fd92e4336a67338e3c485e Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 15 Jul 2022 16:48:44 -0700 Subject: [PATCH 12/14] Add .php as a valid isHTML extension (#629) Before we fully resolve #519, we can sort out the main hurt from this, which seems to be .php extensions. --- src/core/url.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/url.ts b/src/core/url.ts index 2aa343276..0e45d8f2b 100644 --- a/src/core/url.ts +++ b/src/core/url.ts @@ -25,7 +25,7 @@ export function getExtension(url: URL) { } export function isHTML(url: URL) { - return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml))$/) + return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml|php))$/) } export function isPrefixedBy(baseURL: URL, url: URL) { From 2d5cdda4c030658da21965cb20d2885ca7c3e127 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Fri, 15 Jul 2022 19:53:46 -0400 Subject: [PATCH 13/14] Export Type declarations for `turbo:` events (#452) Various `turbo:`-prefixed events are dispatched as [CustomEvent][] instances with data encoded into the [detail][] property. In TypeScript, that property is encoded as `any`, but the `CustomEvent` type is generic (i.e. `CustomEvent`) where the generic Type argument describes the structure of the `detail` key. This commit introduces types that extend from `CustomEvent` for each event, and exports them from `/core/index.ts`, which is exported from `/index.ts` in-turn. In practice, there are no changes to the implementation. However, TypeScript consumers of the package can import the types. At the same time, the internal implementation can depend on the types to ensure consistency throughout. [CustomEvent]: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent [detail]: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail --- src/core/drive/form_submission.ts | 9 +++++++-- src/core/frames/link_interceptor.ts | 8 +++++--- src/core/index.ts | 15 +++++++++++++++ src/core/session.ts | 27 ++++++++++++++++++--------- src/elements/stream_element.ts | 4 +++- src/http/fetch_request.ts | 13 +++++++++++-- src/observers/cache_observer.ts | 6 ++++-- src/observers/stream_observer.ts | 5 +++-- src/util.ts | 11 +++++++---- 9 files changed, 73 insertions(+), 25 deletions(-) diff --git a/src/core/drive/form_submission.ts b/src/core/drive/form_submission.ts index 56a1fc525..244461352 100644 --- a/src/core/drive/form_submission.ts +++ b/src/core/drive/form_submission.ts @@ -29,6 +29,11 @@ enum FormEnctype { plain = "text/plain", } +export type TurboSubmitStartEvent = CustomEvent<{ formSubmission: FormSubmission }> +export type TurboSubmitEndEvent = CustomEvent< + { formSubmission: FormSubmission } & { [K in keyof FormSubmissionResult]?: FormSubmissionResult[K] } +> + function formEnctypeFromString(encoding: string): FormEnctype { switch (encoding.toLowerCase()) { case FormEnctype.multipart: @@ -163,7 +168,7 @@ export class FormSubmission { requestStarted(_request: FetchRequest) { this.state = FormSubmissionState.waiting this.submitter?.setAttribute("disabled", "") - dispatch("turbo:submit-start", { + dispatch("turbo:submit-start", { target: this.formElement, detail: { formSubmission: this }, }) @@ -200,7 +205,7 @@ export class FormSubmission { requestFinished(_request: FetchRequest) { this.state = FormSubmissionState.stopped this.submitter?.removeAttribute("disabled") - dispatch("turbo:submit-end", { + dispatch("turbo:submit-end", { target: this.formElement, detail: { formSubmission: this, ...this.result }, }) diff --git a/src/core/frames/link_interceptor.ts b/src/core/frames/link_interceptor.ts index 53ff4b31b..65ff1066f 100644 --- a/src/core/frames/link_interceptor.ts +++ b/src/core/frames/link_interceptor.ts @@ -1,3 +1,5 @@ +import { TurboClickEvent, TurboBeforeVisitEvent } from "../session" + export interface LinkInterceptorDelegate { shouldInterceptLinkClick(element: Element, url: string): boolean linkClickIntercepted(element: Element, url: string): void @@ -33,7 +35,7 @@ export class LinkInterceptor { } } - linkClicked = ((event: CustomEvent) => { + linkClicked = ((event: TurboClickEvent) => { if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) { if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url)) { this.clickEvent.preventDefault() @@ -44,9 +46,9 @@ export class LinkInterceptor { delete this.clickEvent }) - willVisit = () => { + willVisit = ((_event: TurboBeforeVisitEvent) => { delete this.clickEvent - } + }) respondsToEventTarget(target: EventTarget | null) { const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null diff --git a/src/core/index.ts b/src/core/index.ts index f736483cf..24136d790 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -12,6 +12,21 @@ import { FormSubmission } from "./drive/form_submission" const session = new Session() const { navigator } = session export { navigator, session, PageRenderer, PageSnapshot, FrameRenderer } +export { + TurboBeforeCacheEvent, + TurboBeforeRenderEvent, + TurboBeforeVisitEvent, + TurboClickEvent, + TurboFrameLoadEvent, + TurboFrameRenderEvent, + TurboLoadEvent, + TurboRenderEvent, + TurboVisitEvent, +} from "./session" + +export { TurboSubmitStartEvent, TurboSubmitEndEvent } from "./drive/form_submission" +export { TurboBeforeFetchRequestEvent, TurboBeforeFetchResponseEvent } from "../http/fetch_request" +export { TurboBeforeStreamRenderEvent } from "../elements/stream_element" /** * Starts the main session. diff --git a/src/core/session.ts b/src/core/session.ts index 727019ba5..4b723577f 100644 --- a/src/core/session.ts +++ b/src/core/session.ts @@ -21,6 +21,15 @@ import { FetchResponse } from "../http/fetch_response" import { Preloader, PreloaderDelegate } from "./drive/preloader" export type TimingData = unknown +export type TurboBeforeCacheEvent = CustomEvent +export type TurboBeforeRenderEvent = CustomEvent<{ newBody: HTMLBodyElement; resume: (value: any) => void }> +export type TurboBeforeVisitEvent = CustomEvent<{ url: string }> +export type TurboClickEvent = CustomEvent<{ url: string; originalEvent: MouseEvent }> +export type TurboFrameLoadEvent = CustomEvent +export type TurboFrameRenderEvent = CustomEvent<{ fetchResponse: FetchResponse }> +export type TurboLoadEvent = CustomEvent<{ url: string; timing: TimingData }> +export type TurboRenderEvent = CustomEvent +export type TurboVisitEvent = CustomEvent<{ url: string; action: Action }> export class Session implements @@ -311,7 +320,7 @@ export class Session } notifyApplicationAfterClickingLinkToLocation(link: Element, location: URL, event: MouseEvent) { - return dispatch("turbo:click", { + return dispatch("turbo:click", { target: link, detail: { url: location.href, originalEvent: event }, cancelable: true, @@ -319,7 +328,7 @@ export class Session } notifyApplicationBeforeVisitingLocation(location: URL) { - return dispatch("turbo:before-visit", { + return dispatch("turbo:before-visit", { detail: { url: location.href }, cancelable: true, }) @@ -327,27 +336,27 @@ export class Session notifyApplicationAfterVisitingLocation(location: URL, action: Action) { markAsBusy(document.documentElement) - return dispatch("turbo:visit", { detail: { url: location.href, action } }) + return dispatch("turbo:visit", { detail: { url: location.href, action } }) } notifyApplicationBeforeCachingSnapshot() { - return dispatch("turbo:before-cache") + return dispatch("turbo:before-cache") } notifyApplicationBeforeRender(newBody: HTMLBodyElement, resume: (value: any) => void) { - return dispatch("turbo:before-render", { + return dispatch("turbo:before-render", { detail: { newBody, resume }, cancelable: true, }) } notifyApplicationAfterRender() { - return dispatch("turbo:render") + return dispatch("turbo:render") } notifyApplicationAfterPageLoad(timing: TimingData = {}) { clearBusyState(document.documentElement) - return dispatch("turbo:load", { + return dispatch("turbo:load", { detail: { url: this.location.href, timing }, }) } @@ -362,11 +371,11 @@ export class Session } notifyApplicationAfterFrameLoad(frame: FrameElement) { - return dispatch("turbo:frame-load", { target: frame }) + return dispatch("turbo:frame-load", { target: frame }) } notifyApplicationAfterFrameRender(fetchResponse: FetchResponse, frame: FrameElement) { - return dispatch("turbo:frame-render", { + return dispatch("turbo:frame-render", { detail: { fetchResponse }, target: frame, cancelable: true, diff --git a/src/elements/stream_element.ts b/src/elements/stream_element.ts index 8d3475ccf..4e803a3cf 100644 --- a/src/elements/stream_element.ts +++ b/src/elements/stream_element.ts @@ -1,6 +1,8 @@ import { StreamActions } from "../core/streams/stream_actions" import { nextAnimationFrame } from "../util" +export type TurboBeforeStreamRenderEvent = CustomEvent + //