diff --git a/.prettierrc.json b/.prettierrc.json index db5bf647b..ec8e5e030 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,5 +1,6 @@ { "singleQuote": false, "printWidth": 120, - "semi": false + "semi": false, + "trailingComma" : "none" } diff --git a/package.json b/package.json index 7488a73fd..28ef82bff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hotwired/turbo", - "version": "8.0.4", + "version": "8.0.5", "description": "The speed of a single-page web application without having to write any JavaScript", "module": "dist/turbo.es2017-esm.js", "main": "dist/turbo.es2017-umd.js", diff --git a/src/core/drive/morph_renderer.js b/src/core/drive/morph_renderer.js deleted file mode 100644 index 2c5d14874..000000000 --- a/src/core/drive/morph_renderer.js +++ /dev/null @@ -1,118 +0,0 @@ -import { Idiomorph } from "idiomorph/dist/idiomorph.esm.js" -import { dispatch } from "../../util" -import { PageRenderer } from "./page_renderer" - -export class MorphRenderer extends PageRenderer { - async render() { - if (this.willRender) await this.#morphBody() - } - - get renderMethod() { - return "morph" - } - - // Private - - async #morphBody() { - this.#morphElements(this.currentElement, this.newElement) - this.#reloadRemoteFrames() - - dispatch("turbo:morph", { - detail: { - currentElement: this.currentElement, - newElement: this.newElement - } - }) - } - - #morphElements(currentElement, newElement, morphStyle = "outerHTML") { - this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement) - - Idiomorph.morph(currentElement, newElement, { - morphStyle: morphStyle, - callbacks: { - beforeNodeAdded: this.#shouldAddElement, - beforeNodeMorphed: this.#shouldMorphElement, - beforeAttributeUpdated: this.#shouldUpdateAttribute, - beforeNodeRemoved: this.#shouldRemoveElement, - afterNodeMorphed: this.#didMorphElement - } - }) - } - - #shouldAddElement = (node) => { - return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id)) - } - - #shouldMorphElement = (oldNode, newNode) => { - if (oldNode instanceof HTMLElement) { - if (!oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))) { - const event = dispatch("turbo:before-morph-element", { - cancelable: true, - target: oldNode, - detail: { - newElement: newNode - } - }) - - return !event.defaultPrevented - } else { - return false - } - } - } - - #shouldUpdateAttribute = (attributeName, target, mutationType) => { - const event = dispatch("turbo:before-morph-attribute", { cancelable: true, target, detail: { attributeName, mutationType } }) - - return !event.defaultPrevented - } - - #didMorphElement = (oldNode, newNode) => { - if (newNode instanceof HTMLElement) { - dispatch("turbo:morph-element", { - target: oldNode, - detail: { - newElement: newNode - } - }) - } - } - - #shouldRemoveElement = (node) => { - return this.#shouldMorphElement(node) - } - - #reloadRemoteFrames() { - this.#remoteFrames().forEach((frame) => { - if (this.#isFrameReloadedWithMorph(frame)) { - this.#renderFrameWithMorph(frame) - frame.reload() - } - }) - } - - #renderFrameWithMorph(frame) { - frame.addEventListener("turbo:before-frame-render", (event) => { - event.detail.render = this.#morphFrameUpdate - }, { once: true }) - } - - #morphFrameUpdate = (currentElement, newElement) => { - dispatch("turbo:before-frame-morph", { - target: currentElement, - detail: { currentElement, newElement } - }) - this.#morphElements(currentElement, newElement.children, "innerHTML") - } - - #isFrameReloadedWithMorph(element) { - return element.src && element.refresh === "morph" - } - - #remoteFrames() { - return Array.from(document.querySelectorAll('turbo-frame[src]')).filter(frame => { - return !frame.closest('[data-turbo-permanent]') - }) - } -} diff --git a/src/core/drive/morphing_page_renderer.js b/src/core/drive/morphing_page_renderer.js new file mode 100644 index 000000000..2884a24fb --- /dev/null +++ b/src/core/drive/morphing_page_renderer.js @@ -0,0 +1,48 @@ +import { FrameElement } from "../../elements/frame_element" +import { MorphingFrameRenderer } from "../frames/morphing_frame_renderer" +import { PageRenderer } from "./page_renderer" +import { dispatch } from "../../util" +import { morphElements } from "../morphing" + +export class MorphingPageRenderer extends PageRenderer { + static renderElement(currentElement, newElement) { + morphElements(currentElement, newElement, { + callbacks: { + beforeNodeMorphed: element => !canRefreshFrame(element) + } + }) + + for (const frame of currentElement.querySelectorAll("turbo-frame")) { + if (canRefreshFrame(frame)) refreshFrame(frame) + } + + dispatch("turbo:morph", { detail: { currentElement, newElement } }) + } + + async preservingPermanentElements(callback) { + return await callback() + } + + get renderMethod() { + return "morph" + } + + get shouldAutofocus() { + return false + } +} + +function canRefreshFrame(frame) { + return frame instanceof FrameElement && + frame.src && + frame.refresh === "morph" && + !frame.closest("[data-turbo-permanent]") +} + +function refreshFrame(frame) { + frame.addEventListener("turbo:before-frame-render", ({ detail }) => { + detail.render = MorphingFrameRenderer.renderElement + }, { once: true }) + + frame.reload() +} diff --git a/src/core/drive/navigator.js b/src/core/drive/navigator.js index 8d8d3e0be..8210c7471 100644 --- a/src/core/drive/navigator.js +++ b/src/core/drive/navigator.js @@ -125,6 +125,7 @@ export class Navigator { visitCompleted(visit) { this.delegate.visitCompleted(visit) + delete this.currentVisit } locationWithActionIsSamePage(location, action) { diff --git a/src/core/drive/page_view.js b/src/core/drive/page_view.js index 1583f25a0..1bcb04134 100644 --- a/src/core/drive/page_view.js +++ b/src/core/drive/page_view.js @@ -1,7 +1,7 @@ import { nextEventLoopTick } from "../../util" import { View } from "../view" import { ErrorRenderer } from "./error_renderer" -import { MorphRenderer } from "./morph_renderer" +import { MorphingPageRenderer } from "./morphing_page_renderer" import { PageRenderer } from "./page_renderer" import { PageSnapshot } from "./page_snapshot" import { SnapshotCache } from "./snapshot_cache" @@ -17,9 +17,9 @@ export class PageView extends View { renderPage(snapshot, isPreview = false, willRender = true, visit) { const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage - const rendererClass = shouldMorphPage ? MorphRenderer : PageRenderer + const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer - const renderer = new rendererClass(this.snapshot, snapshot, PageRenderer.renderElement, isPreview, willRender) + const renderer = new rendererClass(this.snapshot, snapshot, rendererClass.renderElement, isPreview, willRender) if (!renderer.shouldRender) { this.forceReloaded = true diff --git a/src/core/frames/link_interceptor.js b/src/core/frames/link_interceptor.js index 9b9249544..fb025db58 100644 --- a/src/core/frames/link_interceptor.js +++ b/src/core/frames/link_interceptor.js @@ -1,3 +1,5 @@ +import { findLinkFromClickTarget } from "../../util" + export class LinkInterceptor { constructor(delegate, element) { this.delegate = delegate @@ -17,7 +19,7 @@ export class LinkInterceptor { } clickBubbled = (event) => { - if (this.respondsToEventTarget(event.target)) { + if (this.clickEventIsSignificant(event)) { this.clickEvent = event } else { delete this.clickEvent @@ -25,7 +27,7 @@ export class LinkInterceptor { } linkClicked = (event) => { - if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) { + if (this.clickEvent && this.clickEventIsSignificant(event)) { if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) { this.clickEvent.preventDefault() event.preventDefault() @@ -39,8 +41,10 @@ export class LinkInterceptor { delete this.clickEvent } - respondsToEventTarget(target) { - const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null - return element && element.closest("turbo-frame, html") == this.element + clickEventIsSignificant(event) { + const target = event.composed ? event.target?.parentElement : event.target + const element = findLinkFromClickTarget(target) || target + + return element instanceof Element && element.closest("turbo-frame, html") == this.element } } diff --git a/src/core/frames/morphing_frame_renderer.js b/src/core/frames/morphing_frame_renderer.js new file mode 100644 index 000000000..fe42065d5 --- /dev/null +++ b/src/core/frames/morphing_frame_renderer.js @@ -0,0 +1,14 @@ +import { FrameRenderer } from "./frame_renderer" +import { morphChildren } from "../morphing" +import { dispatch } from "../../util" + +export class MorphingFrameRenderer extends FrameRenderer { + static renderElement(currentElement, newElement) { + dispatch("turbo:before-frame-morph", { + target: currentElement, + detail: { currentElement, newElement } + }) + + morphChildren(currentElement, newElement) + } +} diff --git a/src/core/morphing.js b/src/core/morphing.js new file mode 100644 index 000000000..dfd21e839 --- /dev/null +++ b/src/core/morphing.js @@ -0,0 +1,66 @@ +import { Idiomorph } from "idiomorph/dist/idiomorph.esm.js" +import { dispatch } from "../util" + +export function morphElements(currentElement, newElement, { callbacks, ...options } = {}) { + Idiomorph.morph(currentElement, newElement, { + ...options, + callbacks: new DefaultIdiomorphCallbacks(callbacks) + }) +} + +export function morphChildren(currentElement, newElement) { + morphElements(currentElement, newElement.children, { + morphStyle: "innerHTML" + }) +} + +class DefaultIdiomorphCallbacks { + #beforeNodeMorphed + + constructor({ beforeNodeMorphed } = {}) { + this.#beforeNodeMorphed = beforeNodeMorphed || (() => true) + } + + beforeNodeAdded = (node) => { + return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id)) + } + + beforeNodeMorphed = (currentElement, newElement) => { + if (currentElement instanceof Element) { + if (!currentElement.hasAttribute("data-turbo-permanent") && this.#beforeNodeMorphed(currentElement, newElement)) { + const event = dispatch("turbo:before-morph-element", { + cancelable: true, + target: currentElement, + detail: { currentElement, newElement } + }) + + return !event.defaultPrevented + } else { + return false + } + } + } + + beforeAttributeUpdated = (attributeName, target, mutationType) => { + const event = dispatch("turbo:before-morph-attribute", { + cancelable: true, + target, + detail: { attributeName, mutationType } + }) + + return !event.defaultPrevented + } + + beforeNodeRemoved = (node) => { + return this.beforeNodeMorphed(node) + } + + afterNodeMorphed = (currentElement, newElement) => { + if (currentElement instanceof Element) { + dispatch("turbo:morph-element", { + target: currentElement, + detail: { currentElement, newElement } + }) + } + } +} diff --git a/src/core/renderer.js b/src/core/renderer.js index 56e73983e..f0a02f581 100644 --- a/src/core/renderer.js +++ b/src/core/renderer.js @@ -16,6 +16,10 @@ export class Renderer { return true } + get shouldAutofocus() { + return true + } + get reloadReason() { return } @@ -40,9 +44,11 @@ export class Renderer { } focusFirstAutofocusableElement() { - const element = this.connectedSnapshot.firstAutofocusableElement - if (element) { - element.focus() + if (this.shouldAutofocus) { + const element = this.connectedSnapshot.firstAutofocusableElement + if (element) { + element.focus() + } } } diff --git a/src/core/session.js b/src/core/session.js index cdb978348..1047d4463 100644 --- a/src/core/session.js +++ b/src/core/session.js @@ -109,7 +109,7 @@ export class Session { refresh(url, requestId) { const isRecentRequest = requestId && this.recentRequests.has(requestId) - if (!isRecentRequest) { + if (!isRecentRequest && !this.navigator.currentVisit) { this.visit(url, { action: "replace", shouldCacheSnapshot: false }) } } diff --git a/src/core/streams/actions/morph.js b/src/core/streams/actions/morph.js deleted file mode 100644 index d177bfd01..000000000 --- a/src/core/streams/actions/morph.js +++ /dev/null @@ -1,63 +0,0 @@ -import { Idiomorph } from "idiomorph/dist/idiomorph.esm" -import { dispatch } from "../../../util" - -export default function morph(streamElement) { - const morphStyle = streamElement.hasAttribute("children-only") ? "innerHTML" : "outerHTML" - streamElement.targetElements.forEach((element) => { - Idiomorph.morph(element, streamElement.templateContent, { - morphStyle: morphStyle, - callbacks: { - beforeNodeAdded, - beforeNodeMorphed, - beforeAttributeUpdated, - beforeNodeRemoved, - afterNodeMorphed - } - }) - }) -} - -function beforeNodeAdded(node) { - return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id)) -} - -function beforeNodeRemoved(node) { - return beforeNodeAdded(node) -} - -function beforeNodeMorphed(target, newElement) { - if (target instanceof HTMLElement && !target.hasAttribute("data-turbo-permanent")) { - const event = dispatch("turbo:before-morph-element", { - cancelable: true, - target, - detail: { - newElement - } - }) - return !event.defaultPrevented - } - return false -} - -function beforeAttributeUpdated(attributeName, target, mutationType) { - const event = dispatch("turbo:before-morph-attribute", { - cancelable: true, - target, - detail: { - attributeName, - mutationType - } - }) - return !event.defaultPrevented -} - -function afterNodeMorphed(target, newElement) { - if (newElement instanceof HTMLElement) { - dispatch("turbo:morph-element", { - target, - detail: { - newElement - } - }) - } -} diff --git a/src/core/streams/stream_actions.js b/src/core/streams/stream_actions.js index 486dc8566..a038add0d 100644 --- a/src/core/streams/stream_actions.js +++ b/src/core/streams/stream_actions.js @@ -1,5 +1,5 @@ import { session } from "../" -import morph from "./actions/morph" +import { morphElements, morphChildren } from "../morphing" export const StreamActions = { after() { @@ -25,21 +25,31 @@ export const StreamActions = { }, replace() { - this.targetElements.forEach((e) => e.replaceWith(this.templateContent)) + const method = this.getAttribute("method") + + this.targetElements.forEach((targetElement) => { + if (method === "morph") { + morphElements(targetElement, this.templateContent) + } else { + targetElement.replaceWith(this.templateContent) + } + }) }, update() { + const method = this.getAttribute("method") + this.targetElements.forEach((targetElement) => { - targetElement.innerHTML = "" - targetElement.append(this.templateContent) + if (method === "morph") { + morphChildren(targetElement, this.templateContent) + } else { + targetElement.innerHTML = "" + targetElement.append(this.templateContent) + } }) }, refresh() { session.refresh(this.baseURI, this.requestId) - }, - - morph() { - morph(this) } } diff --git a/src/elements/stream_element.js b/src/elements/stream_element.js index b9bcf35f0..77f447594 100644 --- a/src/elements/stream_element.js +++ b/src/elements/stream_element.js @@ -6,20 +6,22 @@ import { nextRepaint } from "../util" /** * Renders updates to the page from a stream of messages. * - * Using the `action` attribute, this can be configured one of four ways: + * Using the `action` attribute, this can be configured one of eight ways: * - * - `append` - appends the result to the container - * - `prepend` - prepends the result to the container - * - `replace` - replaces the contents of the container - * - `remove` - removes the container - * - `before` - inserts the result before the target * - `after` - inserts the result after the target + * - `append` - appends the result to the target + * - `before` - inserts the result before the target + * - `prepend` - prepends the result to the target + * - `refresh` - initiates a page refresh + * - `remove` - removes the target + * - `replace` - replaces the outer HTML of the target + * - `update` - replaces the inner HTML of the target * * @customElement turbo-stream * @example * * * */ diff --git a/src/http/fetch_request.js b/src/http/fetch_request.js index f4ff5adbe..f39fd8101 100644 --- a/src/http/fetch_request.js +++ b/src/http/fetch_request.js @@ -56,7 +56,7 @@ export class FetchRequest { this.fetchOptions = { credentials: "same-origin", redirect: "follow", - method: method, + method: method.toUpperCase(), headers: { ...this.defaultHeaders }, body: body, signal: this.abortSignal, @@ -79,7 +79,7 @@ export class FetchRequest { this.url = url this.fetchOptions.body = body - this.fetchOptions.method = fetchMethod + this.fetchOptions.method = fetchMethod.toUpperCase() } get headers() { diff --git a/src/observers/form_submit_observer.js b/src/observers/form_submit_observer.js index 1158d59b9..204e4665a 100644 --- a/src/observers/form_submit_observer.js +++ b/src/observers/form_submit_observer.js @@ -1,3 +1,5 @@ +import { doesNotTargetIFrame } from "../util" + export class FormSubmitObserver { started = false @@ -51,15 +53,7 @@ function submissionDoesNotDismissDialog(form, submitter) { } function submissionDoesNotTargetIFrame(form, submitter) { - if (submitter?.hasAttribute("formtarget") || form.hasAttribute("target")) { - const target = submitter?.getAttribute("formtarget") || form.target - - for (const element of document.getElementsByName(target)) { - if (element instanceof HTMLIFrameElement) return false - } + const target = submitter?.getAttribute("formtarget") || form.getAttribute("target") - return true - } else { - return true - } + return doesNotTargetIFrame(target) } diff --git a/src/observers/link_click_observer.js b/src/observers/link_click_observer.js index 24c6aa235..c1e6abfb7 100644 --- a/src/observers/link_click_observer.js +++ b/src/observers/link_click_observer.js @@ -31,7 +31,7 @@ export class LinkClickObserver { if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) { const target = (event.composedPath && event.composedPath()[0]) || event.target const link = findLinkFromClickTarget(target) - if (link && doesNotTargetIFrame(link)) { + if (link && doesNotTargetIFrame(link.target)) { const location = getLocationForLink(link) if (this.delegate.willFollowLinkToLocation(link, location, event)) { event.preventDefault() diff --git a/src/observers/link_prefetch_observer.js b/src/observers/link_prefetch_observer.js index 6265f075a..04c583ab1 100644 --- a/src/observers/link_prefetch_observer.js +++ b/src/observers/link_prefetch_observer.js @@ -93,7 +93,7 @@ export class LinkPrefetchObserver { } #tryToUsePrefetchedRequest = (event) => { - if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") { + if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") { const cached = prefetchCache.get(event.detail.url.toString()) if (cached) { diff --git a/src/tests/fixtures/422_morph.html b/src/tests/fixtures/422_morph.html new file mode 100644 index 000000000..d87ec4161 --- /dev/null +++ b/src/tests/fixtures/422_morph.html @@ -0,0 +1,16 @@ + + + + + + Unprocessable Entity + + + +

Unprocessable Entity

+ + +

Frame: Unprocessable Entity

+
+ + diff --git a/src/tests/fixtures/autofocus.html b/src/tests/fixtures/autofocus.html index a0d55fa0d..8d49539cb 100644 --- a/src/tests/fixtures/autofocus.html +++ b/src/tests/fixtures/autofocus.html @@ -5,6 +5,8 @@ Autofocus + +

Autofocus

@@ -22,5 +24,12 @@

Autofocus

#drives-frame link to frames/form.html + +
+ + + + +
diff --git a/src/tests/fixtures/frames.html b/src/tests/fixtures/frames.html index 24c3272b9..0513e911a 100644 --- a/src/tests/fixtures/frames.html +++ b/src/tests/fixtures/frames.html @@ -37,6 +37,9 @@

Frames: #frame

Navigate #frame to /frames/form.html + + Has a turbo-frame child + Navigate #frame from outside with a[data-turbo-action="advance"]
diff --git a/src/tests/fixtures/hover_to_prefetch.html b/src/tests/fixtures/hover_to_prefetch.html index d47be49db..bd0f6f944 100644 --- a/src/tests/fixtures/hover_to_prefetch.html +++ b/src/tests/fixtures/hover_to_prefetch.html @@ -4,6 +4,7 @@ Hover to Prefetch + diff --git a/src/tests/fixtures/morph_stream_action.html b/src/tests/fixtures/morph_stream_action.html deleted file mode 100644 index df91274f5..000000000 --- a/src/tests/fixtures/morph_stream_action.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - Morph Stream Action - - - - - - -
-
Morph me
-
- - diff --git a/src/tests/fixtures/navigation.html b/src/tests/fixtures/navigation.html index 7b698a613..cd43d2b06 100644 --- a/src/tests/fixtures/navigation.html +++ b/src/tests/fixtures/navigation.html @@ -94,24 +94,30 @@

Navigation

Delayed failure link

Targets iframe[name="iframe"]

Targets iframe[name=""]

+

+ + +

+

- +

- +

- + +

- +

Redirect to cache_observer.html

diff --git a/src/tests/fixtures/page_refresh.html b/src/tests/fixtures/page_refresh.html index c0586677f..2b5a7add9 100644 --- a/src/tests/fixtures/page_refresh.html +++ b/src/tests/fixtures/page_refresh.html @@ -81,6 +81,7 @@

Page to be refreshed

Reload + Navigate with delayed response

Frame to be morphed

@@ -131,7 +132,7 @@

Element with Stimulus controller

-
+
diff --git a/src/tests/fixtures/stream.html b/src/tests/fixtures/stream.html index 85e8c94e4..a2b78907d 100644 --- a/src/tests/fixtures/stream.html +++ b/src/tests/fixtures/stream.html @@ -39,5 +39,9 @@
+ +
+
Morph me
+
diff --git a/src/tests/fixtures/test.js b/src/tests/fixtures/test.js index 7ba0f39e7..0501e2e11 100644 --- a/src/tests/fixtures/test.js +++ b/src/tests/fixtures/test.js @@ -73,6 +73,7 @@ "turbo:before-visit", "turbo:load", "turbo:render", + "turbo:before-prefetch", "turbo:before-fetch-request", "turbo:submit-start", "turbo:submit-end", @@ -91,6 +92,10 @@ "turbo:reload" ]) +window.visitLogs = [] + +addEventListener("turbo:visit", ({ detail }) => window.visitLogs.push(detail)) + customElements.define( "custom-link-element", class extends HTMLElement { diff --git a/src/tests/functional/autofocus_tests.js b/src/tests/functional/autofocus_tests.js index 9cc49afe3..a504d1bc9 100644 --- a/src/tests/functional/autofocus_tests.js +++ b/src/tests/functional/autofocus_tests.js @@ -1,4 +1,5 @@ import { expect, test } from "@playwright/test" +import { nextEventNamed, nextPageRefresh } from "../helpers/page" test.beforeEach(async ({ page }) => { await page.goto("/src/tests/fixtures/autofocus.html") @@ -59,7 +60,9 @@ test("autofocus from a Turbo Stream message does not leak a placeholder [id]", a `) }) + await expect(page.locator("#container-from-stream input")).toBeFocused() + }) test("receiving a Turbo Stream message with an [autofocus] element when an element within the document has focus", async ({ page }) => { @@ -72,3 +75,24 @@ test("receiving a Turbo Stream message with an [autofocus] element when an eleme }) await expect(page.locator("#first-autofocus-element")).toBeFocused() }) + +test("don't focus on [autofocus] elements on page refreshes with morphing", async ({ page }) => { + const input = await page.locator("#form input[autofocus]") + + const button = page.locator("#first-autofocus-element") + await button.click() + + await nextPageRefresh(page) + + await expect(button).toBeFocused() + await expect(input).not.toBeFocused() + + await page.evaluate(() => { + document.querySelector("#form").requestSubmit() + }) + + await nextEventNamed(page, "turbo:render", { renderMethod: "morph" }) + await nextPageRefresh(page) + + await expect(button).toBeFocused() +}) diff --git a/src/tests/functional/form_submission_tests.js b/src/tests/functional/form_submission_tests.js index e7447944f..32d9366dd 100644 --- a/src/tests/functional/form_submission_tests.js +++ b/src/tests/functional/form_submission_tests.js @@ -188,7 +188,7 @@ test("supports transforming a POST submission to a GET in a turbo:submit-start l test("supports transforming a GET submission to a POST in a turbo:submit-start listener", async ({ page }) => { await page.evaluate(() => addEventListener("turbo:submit-start", (({ detail }) => { - detail.formSubmission.method = "post" + detail.formSubmission.method = "POST" detail.formSubmission.body.set("path", "/src/tests/fixtures/one.html") detail.formSubmission.body.set("greeting", "Hello, from an event listener") })) @@ -992,7 +992,7 @@ test("link method form submission submits a single request", async ({ page }) => const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") assert.ok(await noNextEventNamed(page, "turbo:before-fetch-request")) - assert.equal(fetchOptions.method, "post", "[data-turbo-method] overrides the GET method") + assert.equal(fetchOptions.method, "POST", "[data-turbo-method] overrides the GET method") assert.equal(requestCounter, 1, "submits a single HTTP request") }) @@ -1006,7 +1006,7 @@ test("link method form submission inside frame submits a single request", async const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") assert.ok(await noNextEventNamed(page, "turbo:before-fetch-request")) - assert.equal(fetchOptions.method, "post", "[data-turbo-method] overrides the GET method") + assert.equal(fetchOptions.method, "POST", "[data-turbo-method] overrides the GET method") assert.equal(requestCounter, 1, "submits a single HTTP request") }) @@ -1020,7 +1020,7 @@ test("link method form submission targeting frame submits a single request", asy const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") assert.ok(await noNextEventNamed(page, "turbo:before-fetch-request")) - assert.equal(fetchOptions.method, "post", "[data-turbo-method] overrides the GET method") + assert.equal(fetchOptions.method, "POST", "[data-turbo-method] overrides the GET method") assert.equal(requestCounter, 2, "submits a single HTTP request then follows a redirect") }) diff --git a/src/tests/functional/frame_tests.js b/src/tests/functional/frame_tests.js index f1925908b..3b0770c39 100644 --- a/src/tests/functional/frame_tests.js +++ b/src/tests/functional/frame_tests.js @@ -454,6 +454,19 @@ test("'turbo:frame-render' is triggered after frame has finished rendering", asy assert.include(fetchResponse.response.url, "/src/tests/fixtures/frames/part.html") }) +test("navigating a frame from an outer link with a turbo-frame child fires events", async ({ page }) => { + await page.click("#outside-frame-link-with-frame-child") + + await nextEventOnTarget(page, "frame", "turbo:before-fetch-request") + await nextEventOnTarget(page, "frame", "turbo:before-fetch-response") + const { fetchResponse } = await nextEventOnTarget(page, "frame", "turbo:frame-render") + expect(fetchResponse.response.url).toContain("/src/tests/fixtures/frames/form.html") + + await nextEventOnTarget(page, "frame", "turbo:frame-load") + + expect(await readEventLogs(page), "no more events").toHaveLength(0) +}) + test("navigating a frame from an outer form fires events", async ({ page }) => { await page.click("#outside-frame-form") diff --git a/src/tests/functional/link_prefetch_observer_tests.js b/src/tests/functional/link_prefetch_observer_tests.js index 735efb7fb..c4bcd907c 100644 --- a/src/tests/functional/link_prefetch_observer_tests.js +++ b/src/tests/functional/link_prefetch_observer_tests.js @@ -1,6 +1,6 @@ -import { test } from "@playwright/test" +import { expect, test } from "@playwright/test" import { assert } from "chai" -import { nextBeat, sleep } from "../helpers/page" +import { nextBeat, nextEventOnTarget, noNextEventNamed, noNextEventOnTarget, sleep } from "../helpers/page" import fs from "fs" import path from "path" @@ -17,7 +17,22 @@ test.afterEach(() => { test("it prefetches the page", async ({ page }) => { await goTo({ page, path: "/hover_to_prefetch.html" }) - await assertPrefetchedOnHover({ page, selector: "#anchor_for_prefetch" }) + + const link = page.locator("#anchor_for_prefetch") + + await link.hover() + await nextEventOnTarget(page, "anchor_for_prefetch", "turbo:before-prefetch") + const { url, fetchOptions } = await nextEventOnTarget(page, "anchor_for_prefetch", "turbo:before-fetch-request") + + expect(url).toEqual(await link.evaluate(a => a.href)) + expect(fetchOptions.headers["X-Sec-Purpose"]).toEqual("prefetch") + + await link.hover() + await noNextEventOnTarget(page, "anchor_for_prefetch", "turbo:before-fetch-request") + await link.click() + await noNextEventOnTarget(page, "anchor_for_prefetch", "turbo:before-fetch-request") + + await expect(page.locator("body")).toHaveText("Prefetched Page Content") }) test("it doesn't follow the link", async ({ page }) => { @@ -65,17 +80,16 @@ test("it doesn't prefetch the page when link has data-turbo=false", async ({ pag test("allows to cancel prefetch requests with custom logic", async ({ page }) => { await goTo({ page, path: "/hover_to_prefetch.html" }) - await assertPrefetchedOnHover({ page, selector: "#anchor_for_prefetch" }) + const link = page.locator("#anchor_for_prefetch") + await link.evaluate(a => a.addEventListener("turbo:before-prefetch", event => event.preventDefault())) - await page.evaluate(() => { - document.body.addEventListener("turbo:before-prefetch", (event) => { - if (event.target.hasAttribute("data-remote")) { - event.preventDefault() - } - }) - }) + await page.pause() + await link.hover() + await nextEventOnTarget(page, "anchor_for_prefetch", "turbo:before-prefetch") + await noNextEventNamed(page, "turbo:before-fetch-request") + await link.click() - await assertNotPrefetchedOnHover({ page, selector: "#anchor_for_prefetch" }) + await expect(page.locator("body")).toHaveText("Prefetched Page Content") }) test("it doesn't prefetch UJS links", async ({ page }) => { @@ -195,35 +209,28 @@ test("doesn't include a turbo-frame header when the link is inside a turbo frame test("it prefetches links with a delay", async ({ page }) => { await goTo({ page, path: "/hover_to_prefetch.html" }) - let requestMade = false - page.on("request", async (request) => (requestMade = true)) - - await page.hover("#anchor_for_prefetch") - await sleep(75) - - assertRequestNotMade(requestMade) - - await sleep(100) + await assertRequestNotMade(page, async () => { + await page.hover("#anchor_for_prefetch") + await sleep(75) + }) - assertRequestMade(requestMade) + await assertRequestMade(page, async () => { + await sleep(100) + }) }) test("it cancels the prefetch request if the link is no longer hovered", async ({ page }) => { await goTo({ page, path: "/hover_to_prefetch.html" }) - let requestMade = false - page.on("request", async (request) => (requestMade = true)) - - await page.hover("#anchor_for_prefetch") - await sleep(75) - - assertRequestNotMade(requestMade) - - await page.mouse.move(0, 0) - - await sleep(100) + await assertRequestNotMade(page, async () => { + await page.hover("#anchor_for_prefetch") + await sleep(75) + }) - assertRequestNotMade(requestMade) + await assertRequestNotMade(page, async () => { + await page.mouse.move(0, 0) + await sleep(100) + }) }) test("it resets the cache when a link is hovered", async ({ page }) => { @@ -246,11 +253,8 @@ test("it resets the cache when a link is hovered", async ({ page }) => { test("it does not make a network request when clicking on a link that has been prefetched", async ({ page }) => { await goTo({ page, path: "/hover_to_prefetch.html" }) - await hoverSelector({ page, selector: "#anchor_for_prefetch" }) - - await sleep(100) - - await assertNotPrefetchedOnHover({ page, selector: "#anchor_for_prefetch" }) + await assertPrefetchedOnHover({ page, selector: "#anchor_for_prefetch" }) + await assertRequestNotMadeOnClick({ page, selector: "#anchor_for_prefetch" }) }) test("it follows the link using the cached response when clicking on a link that has been prefetched", async ({ @@ -264,44 +268,51 @@ test("it follows the link using the cached response when clicking on a link that }) const assertPrefetchedOnHover = async ({ page, selector, callback }) => { - let requestMade = false + await assertRequestMade(page, async () => { + await hoverSelector({ page, selector }) - page.on("request", (request) => { - requestMade = request + await sleep(100) + }, callback) +} + +const assertNotPrefetchedOnHover = async ({ page, selector, callback }) => { + await assertRequestNotMade(page, async () => { + await hoverSelector({ page, selector }) + + await sleep(100) + }, callback) +} + +const assertRequestNotMadeOnClick = async ({ page, selector }) => { + await assertRequestNotMade(page, async () => { + await clickSelector({ page, selector }) }) +} - await hoverSelector({ page, selector }) +const assertRequestMade = async (page, action, callback) => { + let requestMade = false + page.on("request", async (request) => requestMade = request) + + await action() - await sleep(100) + assert.equal(!!requestMade, true, "Network request wasn't made when it should have been.") if (callback) { await callback(requestMade) } - - assertRequestMade(!!requestMade) } -const assertNotPrefetchedOnHover = async ({ page, selector, callback }) => { +const assertRequestNotMade = async (page, action, callback) => { let requestMade = false + page.on("request", async (request) => requestMade = request) - page.on("request", (request) => { - callback && callback(request) - requestMade = true - }) - - await hoverSelector({ page, selector }) + await action() - await sleep(100) + assert.equal(!!requestMade, false, "Network request was made when it should not have been.") - assert.equal(requestMade, false, "Network request was made when it should not have been.") -} - -const assertRequestMade = (requestMade) => { - assert.equal(requestMade, true, "Network request wasn't made when it should have been.") -} - -const assertRequestNotMade = (requestMade) => { - assert.equal(requestMade, false, "Network request was made when it should not have been.") + if (callback) { + await callback(requestMade) + } } const goTo = async ({ page, path }) => { diff --git a/src/tests/functional/morph_stream_action_tests.js b/src/tests/functional/morph_stream_action_tests.js deleted file mode 100644 index b4f04c9d7..000000000 --- a/src/tests/functional/morph_stream_action_tests.js +++ /dev/null @@ -1,48 +0,0 @@ -import { test, expect } from "@playwright/test" -import { nextEventOnTarget, noNextEventOnTarget } from "../helpers/page" - -test("dispatches a turbo:before-morph-element & turbo:morph-element for each morph stream action", async ({ page }) => { - await page.goto("/src/tests/fixtures/morph_stream_action.html") - - await page.evaluate(() => { - window.Turbo.renderStreamMessage(` - - - - `) - }) - - await nextEventOnTarget(page, "message_1", "turbo:before-morph-element") - await nextEventOnTarget(page, "message_1", "turbo:morph-element") - await expect(page.locator("#message_1")).toHaveText("Morphed") -}) - -test("preventing a turbo:before-morph-element prevents the morph", async ({ page }) => { - await page.goto("/src/tests/fixtures/morph_stream_action.html") - - await page.evaluate(() => { - addEventListener("turbo:before-morph-element", (event) => { - event.preventDefault() - }) - }) - - await page.evaluate(() => { - window.Turbo.renderStreamMessage(` - - - - `) - }) - - await nextEventOnTarget(page, "message_1", "turbo:before-morph-element") - await noNextEventOnTarget(page, "message_1", "turbo:morph-element") - await expect(page.locator("#message_1")).toHaveText("Morph me") -}) diff --git a/src/tests/functional/navigation_tests.js b/src/tests/functional/navigation_tests.js index e1aa7e757..eebef546c 100644 --- a/src/tests/functional/navigation_tests.js +++ b/src/tests/functional/navigation_tests.js @@ -1,4 +1,4 @@ -import { test } from "@playwright/test" +import { expect, test } from "@playwright/test" import { assert } from "chai" import { clickWithoutScrolling, @@ -473,6 +473,12 @@ test("ignores links with a [target] attribute that targets an iframe with [name= assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") }) +test("ignores forms with a [target=_blank] attribute", async ({ page }) => { + const [popup] = await Promise.all([page.waitForEvent("popup"), page.click("#form-target-blank button")]) + + expect(pathname(popup.url())).toContain("/src/tests/fixtures/one.html") +}) + test("ignores forms with a [target] attribute that targets an iframe with a matching [name]", async ({ page }) => { await page.click("#form-target-iframe button") await nextBeat() @@ -482,6 +488,12 @@ test("ignores forms with a [target] attribute that targets an iframe with a matc assert.equal(await pathnameForIFrame(page, "iframe"), "/src/tests/fixtures/one.html") }) +test("ignores forms with a button[formtarget=_blank] attribute", async ({ page }) => { + const [popup] = await Promise.all([page.waitForEvent("popup"), page.click("#button-formtarget-blank")]) + + expect(pathname(popup.url())).toContain("/src/tests/fixtures/one.html") +}) + test("ignores forms with a button[formtarget] attribute that targets an iframe with [name='']", async ({ page }) => { diff --git a/src/tests/functional/page_refresh_tests.js b/src/tests/functional/page_refresh_tests.js index c5c116c08..0b6945063 100644 --- a/src/tests/functional/page_refresh_tests.js +++ b/src/tests/functional/page_refresh_tests.js @@ -8,7 +8,8 @@ import { nextEventOnTarget, noNextEventOnTarget, noNextEventNamed, - getSearchParam + getSearchParam, + refreshWithStream } from "../helpers/page" test("renders a page refresh with morphing", async ({ page }) => { @@ -26,16 +27,32 @@ test("async page refresh with turbo-stream", async ({ page }) => { await page.evaluate(() => document.querySelector("#title").innerText = "Updated") await expect(page.locator("#title")).toHaveText("Updated") - - await page.evaluate(() => { - document.body.insertAdjacentHTML("beforeend", ``) - }) + await refreshWithStream(page) await expect(page.locator("#title")).not.toHaveText("Updated") await expect(page.locator("#title")).toHaveText("Page to be refreshed") expect(await noNextEventNamed(page, "turbo:before-cache")).toBeTruthy() }) +test("async page refresh with turbo-stream sequentially initiate Visits", async ({ page }) => { + await page.goto("/src/tests/fixtures/page_refresh.html") + await refreshWithStream(page) + await nextEventNamed(page, "turbo:morph") + await nextEventNamed(page, "turbo:load") + + await refreshWithStream(page) + await nextEventNamed(page, "turbo:morph") + await nextEventNamed(page, "turbo:load") +}) + +test("async page refresh with turbo-stream does not interrupt an initiated Visit", async ({ page }) => { + await page.goto("/src/tests/fixtures/page_refresh.html") + await page.click("#delayed_link") + await refreshWithStream(page) + + await expect(page.locator("h1")).toHaveText("One") +}) + test("dispatches a turbo:before-morph-element and turbo:morph-element event for each morphed element", async ({ page }) => { await page.goto("/src/tests/fixtures/page_refresh.html") await page.fill("#form-text", "Morph me") @@ -109,7 +126,7 @@ test("renders a page refresh with morphing when the paths are the same but searc await nextEventNamed(page, "turbo:render", { renderMethod: "morph" }) }) -test("renders a page refresh with morphing when the GET form paths are the same but search params are diferent", async ({ page }) => { +test("renders a page refresh with morphing when the GET form paths are the same but search params are different", async ({ page }) => { await page.goto("/src/tests/fixtures/page_refresh.html") const input = page.locator("form[method=get] input[name=query]") diff --git a/src/tests/functional/stream_tests.js b/src/tests/functional/stream_tests.js index 40f464d21..5c77016d8 100644 --- a/src/tests/functional/stream_tests.js +++ b/src/tests/functional/stream_tests.js @@ -1,9 +1,11 @@ -import { test } from "@playwright/test" +import { expect, test } from "@playwright/test" import { assert } from "chai" import { hasSelector, nextBeat, nextEventNamed, + nextEventOnTarget, + noNextEventOnTarget, readEventLogs, waitUntilNoSelector, waitUntilText @@ -182,6 +184,48 @@ test("receiving a remove stream message preserves focus blurs the activeElement" assert.notOk(await hasSelector(page, ":focus")) }) +test("dispatches a turbo:before-morph-element & turbo:morph-element for each morph stream action", async ({ page }) => { + await page.evaluate(() => { + window.Turbo.renderStreamMessage(` + + + + `) + }) + + await nextEventOnTarget(page, "message_1", "turbo:before-morph-element") + await nextEventOnTarget(page, "message_1", "turbo:morph-element") + await expect(page.locator("#message_1")).toHaveText("Morphed") +}) + +test("preventing a turbo:before-morph-element prevents the morph", async ({ page }) => { + await page.evaluate(() => { + addEventListener("turbo:before-morph-element", (event) => { + event.preventDefault() + }) + }) + + await page.evaluate(() => { + window.Turbo.renderStreamMessage(` + + + + `) + }) + + await nextEventOnTarget(page, "message_1", "turbo:before-morph-element") + await noNextEventOnTarget(page, "message_1", "turbo:morph-element") + await expect(page.locator("#message_1")).toHaveText("Morph me") +}) + async function getReadyState(page, id) { return page.evaluate((id) => { const element = document.getElementById(id) diff --git a/src/tests/functional/visit_tests.js b/src/tests/functional/visit_tests.js index ec3dec8b9..139ca9d15 100644 --- a/src/tests/functional/visit_tests.js +++ b/src/tests/functional/visit_tests.js @@ -179,7 +179,7 @@ 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.equal(fetchOptions.method, "GET") assert.ok(url.includes("/src/tests/fixtures/one.html")) }) diff --git a/src/tests/helpers/page.js b/src/tests/helpers/page.js index 760b6631f..34069bed7 100644 --- a/src/tests/helpers/page.js +++ b/src/tests/helpers/page.js @@ -226,6 +226,10 @@ export function readMutationLogs(page, length) { return readArray(page, "mutationLogs", length) } +export function refreshWithStream(page) { + return page.evaluate(() => document.body.insertAdjacentHTML("beforeend", ``)) +} + export function search(url) { const { search } = new URL(url) @@ -284,8 +288,10 @@ export function textContent(page, html) { export function visitAction(page) { return page.evaluate(() => { try { - return window.Turbo.navigator.currentVisit.action - } catch (error) { + const lastVisit = window.visitLogs[window.visitLogs.length - 1] + + return lastVisit.action + } catch { return "load" } }) diff --git a/src/tests/server.mjs b/src/tests/server.mjs index e5fc90990..1978ca601 100644 --- a/src/tests/server.mjs +++ b/src/tests/server.mjs @@ -61,6 +61,13 @@ router.post("/reject/tall", (request, response) => { response.status(parseInt(status || "422", 10)).sendFile(fixture) }) +router.post("/reject/morph", (request, response) => { + const { status } = request.body + const fixture = path.join(__dirname, `../../src/tests/fixtures/422_morph.html`) + + response.status(parseInt(status || "422", 10)).sendFile(fixture) +}) + router.post("/reject", (request, response) => { const { status } = request.body const fixture = path.join(__dirname, `../../src/tests/fixtures/${status}.html`) diff --git a/src/tests/unit/stream_element_tests.js b/src/tests/unit/stream_element_tests.js index 1e3b99f92..455581607 100644 --- a/src/tests/unit/stream_element_tests.js +++ b/src/tests/unit/stream_element_tests.js @@ -5,11 +5,12 @@ import { assert } from "@open-wc/testing" import { sleep } from "../helpers/page" import * as Turbo from "../../index" -function createStreamElement(action, target, templateElement) { +function createStreamElement(action, target, templateElement, attributes = {}) { const element = new StreamElement() if (action) element.setAttribute("action", action) if (target) element.setAttribute("target", target) if (templateElement) element.appendChild(templateElement) + Object.entries(attributes).forEach((attribute) => element.setAttribute(...attribute)) return element } @@ -197,9 +198,9 @@ test("test action=refresh discarded when matching request id", async () => { assert.ok(document.body.hasAttribute("data-modified")) }) -test("action=morph", async () => { +test("action=replace method=morph", async () => { const templateElement = createTemplateElement(`

Hello Turbo Morphed

`) - const element = createStreamElement("morph", "hello", templateElement) + const element = createStreamElement("replace", "hello", templateElement, { method: "morph" }) assert.equal(subject.find("div#hello")?.textContent, "Hello Turbo") @@ -210,9 +211,22 @@ test("action=morph", async () => { assert.equal(subject.find("h1#hello")?.textContent, "Hello Turbo Morphed") }) -test("action=morph children-only", async () => { +test("action=replace method=morph with text content change", async () => { + const templateElement = createTemplateElement(`
Hello Turbo Morphed
`) + const element = createStreamElement("replace", "hello", templateElement, { method: "morph" }) + + assert.equal(subject.find("div#hello")?.textContent, "Hello Turbo") + + subject.append(element) + await nextAnimationFrame() + + assert.ok(subject.find("div#hello")) + assert.equal(subject.find("div#hello")?.textContent, "Hello Turbo Morphed") +}) + +test("action=update method=morph", async () => { const templateElement = createTemplateElement(`

Hello Turbo Morphed

`) - const element = createStreamElement("morph", "hello", templateElement) + const element = createStreamElement("update", "hello", templateElement, { method: "morph" }) const target = subject.find("div#hello") assert.equal(target?.textContent, "Hello Turbo") element.setAttribute("children-only", true) diff --git a/src/util.js b/src/util.js index dcb15c31b..57c48ea6a 100644 --- a/src/util.js +++ b/src/util.js @@ -218,14 +218,18 @@ export async function around(callback, reader) { return [before, after] } -export function doesNotTargetIFrame(anchor) { - if (anchor.hasAttribute("target")) { - for (const element of document.getElementsByName(anchor.target)) { +export function doesNotTargetIFrame(name) { + if (name === "_blank") { + return false + } else if (name) { + for (const element of document.getElementsByName(name)) { if (element instanceof HTMLIFrameElement) return false } - } - return true + return true + } else { + return true + } } export function findLinkFromClickTarget(target) { diff --git a/yarn.lock b/yarn.lock index 5b147bcc4..43e3e2743 100644 --- a/yarn.lock +++ b/yarn.lock @@ -796,13 +796,13 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" -body-parser@1.20.1, body-parser@^1.20.1: - version "1.20.1" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" - integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== +body-parser@1.20.2, body-parser@^1.20.1: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== dependencies: bytes "3.1.2" - content-type "~1.0.4" + content-type "~1.0.5" debug "2.6.9" depd "2.0.0" destroy "1.2.0" @@ -810,7 +810,7 @@ body-parser@1.20.1, body-parser@^1.20.1: iconv-lite "0.4.24" on-finished "2.4.1" qs "6.11.0" - raw-body "2.5.1" + raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" @@ -823,11 +823,11 @@ brace-expansion@^1.1.7: concat-map "0.0.1" braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" buffer-crc32@~0.2.3: version "0.2.13" @@ -1061,16 +1061,11 @@ content-disposition@0.5.4, content-disposition@~0.5.2: dependencies: safe-buffer "5.2.1" -content-type@^1.0.4: +content-type@^1.0.4, content-type@~1.0.4, content-type@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== -content-type@~1.0.4: - version "1.0.4" - 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.6.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" @@ -1088,10 +1083,10 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== cookies@~0.8.0: version "0.8.0" @@ -1134,7 +1129,7 @@ debug@2.6.9, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.3.4, debug@^4.3.2: +debug@4, debug@4.3.4, debug@^4.1.1, 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== @@ -1148,13 +1143,6 @@ debug@^3.1.0, debug@^3.2.7: dependencies: ms "^2.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" - deep-eql@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" @@ -1528,16 +1516,16 @@ etag@^1.8.1, etag@~1.8.1: integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== express@^4.18.2: - version "4.18.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" - integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.1" + body-parser "1.20.2" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.5.0" + cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" @@ -1622,10 +1610,10 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -2406,20 +2394,13 @@ object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== -on-finished@2.4.1: +on-finished@2.4.1, on-finished@^2.3.0: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== dependencies: ee-first "1.1.1" -on-finished@^2.3.0: - version "2.3.0" - 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" - once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" @@ -2660,7 +2641,17 @@ range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.5.1, raw-body@^2.3.3: +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +raw-body@^2.3.3: version "2.5.1" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==