From 6d39ceb7fe9cf13a3494ac1ed44e85b942237423 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Fri, 30 Dec 2022 11:41:45 -0500 Subject: [PATCH] Close Issue #834 Closes [hotwired/turbo#834][] When applications embed [Google Adsense][]-powered ` ``` Note the `[name=""]` in the snippet. The guard clauses in the `FormSubmitObserver` and `LinkClickObserver` classes that prevent Turbo Drive from interfering with `` clicks and `
` submissions that target ` Targets [name=""] ``` When clicked, the `` element drives the entire page. In our test suite, there are test cases that cover this behavior, and ensure that Turbo doesn't interfere in these scenarios. [Google Adsense]: https://www.google.com/adsense/start/ [hotwired/turbo#834]: https://github.com/hotwired/turbo/issues/834 [JSFiddle]: https://jsfiddle.net/hk6587oz/ --- src/observers/form_submit_observer.ts | 14 ++++--- src/observers/link_click_observer.ts | 12 ++++-- src/tests/fixtures/navigation.html | 24 ++++++++++- src/tests/functional/navigation_tests.ts | 53 +++++++++++++++++++++++- src/tests/helpers/page.ts | 11 +++++ 5 files changed, 102 insertions(+), 12 deletions(-) diff --git a/src/observers/form_submit_observer.ts b/src/observers/form_submit_observer.ts index 1ef4ca33d..3700d0726 100644 --- a/src/observers/form_submit_observer.ts +++ b/src/observers/form_submit_observer.ts @@ -58,11 +58,15 @@ function submissionDoesNotDismissDialog(form: HTMLFormElement, submitter?: HTMLE } function submissionDoesNotTargetIFrame(form: HTMLFormElement, submitter?: HTMLElement): boolean { - const target = submitter?.getAttribute("formtarget") || form.target + 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 - } + for (const element of document.getElementsByName(target)) { + if (element instanceof HTMLIFrameElement) return false + } - return true + return true + } else { + return true + } } diff --git a/src/observers/link_click_observer.ts b/src/observers/link_click_observer.ts index 6b587c3a5..b5d7c442f 100644 --- a/src/observers/link_click_observer.ts +++ b/src/observers/link_click_observer.ts @@ -71,9 +71,13 @@ export class LinkClickObserver { } function doesNotTargetIFrame(anchor: HTMLAnchorElement): boolean { - for (const element of document.getElementsByName(anchor.target)) { - if (element instanceof HTMLIFrameElement) return false - } + if (anchor.hasAttribute("target")) { + for (const element of document.getElementsByName(anchor.target)) { + if (element instanceof HTMLIFrameElement) return false + } - return true + return true + } else { + return true + } } diff --git a/src/tests/fixtures/navigation.html b/src/tests/fixtures/navigation.html index c78c47d9f..aed08b492 100644 --- a/src/tests/fixtures/navigation.html +++ b/src/tests/fixtures/navigation.html @@ -79,12 +79,34 @@

Navigation

Delayed link

-

Targets iframe

+

Targets iframe[name="iframe"]

+

Targets iframe[name=""]

+

+ + +

+

+

+

+ +
+

+

+

+ +
+

+

+

+ +
+

Redirect to cache_observer.html

+ diff --git a/src/tests/functional/navigation_tests.ts b/src/tests/functional/navigation_tests.ts index c1de7feed..5993e3dc8 100644 --- a/src/tests/functional/navigation_tests.ts +++ b/src/tests/functional/navigation_tests.ts @@ -11,6 +11,7 @@ import { nextEventNamed, noNextEventNamed, pathname, + pathnameForIFrame, readEventLogs, search, selectorHasFocus, @@ -428,9 +429,57 @@ test("test navigating back whilst a visit is in-flight", async ({ page }) => { assert.equal(await visitAction(page), "restore") }) -test("test ignores links that target an iframe", async ({ page }) => { - await page.click("#targets-iframe") +test("test ignores links with a [target] attribute that target an iframe with a matching [name]", async ({ page }) => { + await page.click("#link-target-iframe") await nextBeat() + await noNextEventNamed(page, "turbo:load") assert.equal(pathname(page.url()), "/src/tests/fixtures/navigation.html") + assert.equal(await pathnameForIFrame(page, "iframe"), "/src/tests/fixtures/one.html") +}) + +test("test ignores links with a [target] attribute that targets an iframe with [name='']", async ({ page }) => { + await page.click("#link-target-empty-name-iframe") + await nextBeat() + await noNextEventNamed(page, "turbo:load") + + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") +}) + +test("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() + await noNextEventNamed(page, "turbo:load") + + assert.equal(pathname(page.url()), "/src/tests/fixtures/navigation.html") + assert.equal(await pathnameForIFrame(page, "iframe"), "/src/tests/fixtures/one.html") +}) + +test("test ignores forms with a button[formtarget] attribute that targets an iframe with [name='']", async ({ + page, +}) => { + await page.click("#form-target-empty-name-iframe button") + await nextBeat() + await noNextEventNamed(page, "turbo:load") + + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") +}) + +test("test ignores forms with a button[formtarget] attribute that targets an iframe with a matching [name]", async ({ + page, +}) => { + await page.click("#button-formtarget-iframe") + await nextBeat() + await noNextEventNamed(page, "turbo:load") + + assert.equal(pathname(page.url()), "/src/tests/fixtures/navigation.html") + assert.equal(await pathnameForIFrame(page, "iframe"), "/src/tests/fixtures/one.html") +}) + +test("test ignores forms with a [target] attribute that target an iframe with [name='']", async ({ page }) => { + await page.click("#button-formtarget-empty-name-iframe") + await nextBeat() + await noNextEventNamed(page, "turbo:load") + + assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html") }) diff --git a/src/tests/helpers/page.ts b/src/tests/helpers/page.ts index e9d0e8088..5c25669c4 100644 --- a/src/tests/helpers/page.ts +++ b/src/tests/helpers/page.ts @@ -156,6 +156,17 @@ export function pathname(url: string): string { return pathname } +export async function pathnameForIFrame(page: Page, name: string) { + const locator = await page.locator(`[name="${name}"]`) + const location = await locator.evaluate((iframe: HTMLIFrameElement) => iframe.contentWindow?.location) + + if (location) { + return pathname(location.href) + } else { + return "" + } +} + export function propertyForSelector(page: Page, selector: string, propertyName: string): Promise { return page.locator(selector).evaluate((element, propertyName) => (element as any)[propertyName], propertyName) }