Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,3 +37,6 @@ jobs:

- name: Firefox Test
run: yarn test:browser --project=firefox

- name: Chrome With Trusted Types Test
run: yarn test:browser --project=chrome-with-trusted-types
4 changes: 4 additions & 0 deletions playwright.config.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,10 @@ const config: PlaywrightTestConfig = {
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "chrome-with-trusted-types",
use: { ...devices["Desktop Chrome"] },
},
],
retries: 2,
testDir: "./src/tests/functional",
Expand Down
4 changes: 2 additions & 2 deletions src/core/drive/navigator.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,7 +96,7 @@ export class Navigator {
const visitOptions = {
action,
shouldCacheSnapshot,
response: { statusCode, redirected, response },
response: { statusCode, redirected, response, responseHTML },
}

const location = fetchResponse.location
Expand All@@ -112,7 +112,7 @@ export class Navigator {
const responseHTML = await fetchResponse.responseHTML

if (responseHTML) {
const snapshot = await PageSnapshot.fromResponse(fetchResponse.response)
const snapshot = PageSnapshot.fromResponse(fetchResponse.response, responseHTML)

if (fetchResponse.serverError) {
await this.view.renderError(snapshot, this.currentVisit)
Expand Down
16 changes: 6 additions & 10 deletions src/core/drive/page_snapshot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,17 +5,13 @@ import { expandURL } from "../url"
import { HeadSnapshot } from "./head_snapshot"

export class PageSnapshot extends Snapshot<HTMLBodyElement> {
static async fromResponse(response?: Response): Promise<PageSnapshot> {
if (!response) {
return this.fromHTMLString()
}
const responseText = await response.text()
if (CSPTrustedTypesPolicy == null) {
return this.fromHTMLString(responseText)
} else {
const trustedHTML = CSPTrustedTypesPolicy.createHTML(responseText, response)
return this.fromHTMLString(trustedHTML as string)
static fromResponse(response?: Response, html = emptyHTML): PageSnapshot {
if (!response || CSPTrustedTypesPolicy == null) {
return this.fromHTMLString(html)
}

const trustedHTML = CSPTrustedTypesPolicy.createHTML(html, response)
return this.fromHTMLString(trustedHTML as string)
}

static fromHTMLString(html = emptyHTML) {
Expand Down
3 changes: 2 additions & 1 deletion src/core/drive/preloader.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,7 +43,8 @@ export class Preloader {

try {
const response = await fetch(location.toString(), { headers: { "VND.PREFETCH": "true", Accept: "text/html" } })
const snapshot = await PageSnapshot.fromResponse(response)
const responseText = await response.text()
const snapshot = PageSnapshot.fromResponse(response, responseText)

this.snapshotCache.put(location, snapshot)
} catch (_) {
Expand Down
13 changes: 7 additions & 6 deletions src/core/drive/visit.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,7 @@ export type VisitResponse = {
statusCode: number
redirected: boolean
response?: Response
responseHTML?: string
}

export enum SystemStatusCode {
Expand DownExpand Up@@ -253,17 +254,17 @@ export class Visit implements FetchRequestDelegate {

loadResponse() {
if (this.response) {
const { statusCode, response } = this.response
const { statusCode, response, responseHTML } = this.response
this.render(async () => {
if (this.shouldCacheSnapshot) this.cacheSnapshot()
if (this.view.renderPromise) await this.view.renderPromise
if (isSuccessful(statusCode) && response != null) {
await this.view.renderPage(await PageSnapshot.fromResponse(response), false, this.willRender, this)
if (isSuccessful(statusCode) && responseHTML != null) {
await this.view.renderPage(PageSnapshot.fromResponse(response, responseHTML), false, this.willRender, this)
this.performScroll()
this.adapter.visitRendered(this)
this.complete()
} else {
await this.view.renderError(await PageSnapshot.fromResponse(response), this)
await this.view.renderError(PageSnapshot.fromResponse(response, responseHTML), this)
this.adapter.visitRendered(this)
this.fail()
}
Expand DownExpand Up@@ -360,7 +361,7 @@ export class Visit implements FetchRequestDelegate {
if (this.redirectedToLocation && fetchResponse.location.hash === "") {
this.redirectedToLocation.hash = request.url.hash
}
this.recordResponse({ statusCode: statusCode, redirected, response })
this.recordResponse({ statusCode: statusCode, redirected, response, responseHTML })
}
}

Expand All@@ -373,7 +374,7 @@ export class Visit implements FetchRequestDelegate {
redirected,
})
} else {
this.recordResponse({ statusCode: statusCode, redirected, response })
this.recordResponse({ statusCode: statusCode, redirected, response, responseHTML })
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/core/frames/frame_controller.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -421,7 +421,8 @@ export class FrameController
frame.delegate.fetchResponseLoaded = (fetchResponse: FetchResponse) => {
if (frame.src) {
const { statusCode, redirected, response } = fetchResponse
const visitResponse = { statusCode, redirected, response }
const responseHTML = frame.ownerDocument.documentElement.outerHTML
const visitResponse = { statusCode, redirected, response, responseHTML }
const options: Partial<VisitOptions> = {
response: visitResponse,
visitCachedSnapshot,
Expand DownExpand Up@@ -469,9 +470,10 @@ export class FrameController

private async visitResponse(response: Response): Promise<void> {
const wrapped = new FetchResponse(response)
const responseHTML = await wrapped.responseHTML
const { location, redirected, statusCode } = wrapped

return session.visit(location, { response: { redirected, statusCode, response } })
return session.visit(location, { response: { redirected, statusCode, response, responseHTML } })
}

private findFrameElement(element: Element, submitter?: HTMLElement) {
Expand Down
1 change: 1 addition & 0 deletions src/core/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@ export { TurboSubmitStartEvent, TurboSubmitEndEvent } from "./drive/form_submiss
export { TurboFrameMissingEvent } from "./frames/frame_controller"

export { StreamActions, TurboStreamAction, TurboStreamActions } from "./streams/stream_actions"
export { setCSPTrustedTypesPolicy } from "../trusted_types"

/**
* Starts the main session.
Expand Down
1 change: 1 addition & 0 deletions src/tests/functional/async_script_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { test } from "@playwright/test"
import { assert } from "chai"
import { readEventLogs, visitAction } from "../helpers/page"
Expand Down
1 change: 1 addition & 0 deletions src/tests/functional/autofocus_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { test } from "@playwright/test"
import { assert } from "chai"
import { hasSelector, nextBeat } from "../helpers/page"
Expand Down
1 change: 1 addition & 0 deletions src/tests/functional/cache_observer_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { test } from "@playwright/test"
import { assert } from "chai"
import { hasSelector, nextBody } from "../helpers/page"
Expand Down
1 change: 1 addition & 0 deletions src/tests/functional/drive_disabled_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { test } from "@playwright/test"
import { assert } from "chai"
import {
Expand Down
1 change: 1 addition & 0 deletions src/tests/functional/drive_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { test } from "@playwright/test"
import { assert } from "chai"
import { nextBody, pathname, visitAction } from "../helpers/page"
Expand Down
1 change: 1 addition & 0 deletions src/tests/functional/form_mode_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { Page, test } from "@playwright/test"
import { getFromLocalStorage, setLocalStorageFromEvent } from "../helpers/page"
import { assert } from "chai"
Expand Down
1 change: 1 addition & 0 deletions src/tests/functional/frame_navigation_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { test } from "@playwright/test"
import {
getFromLocalStorage,
Expand Down
1 change: 1 addition & 0 deletions src/tests/functional/frame_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { Page, test } from "@playwright/test"
import { assert, Assertion } from "chai"
import {
Expand Down
1 change: 1 addition & 0 deletions src/tests/functional/import_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { test } from "@playwright/test"
import { assert } from "chai"

Expand Down
1 change: 1 addition & 0 deletions src/tests/functional/loading_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { test } from "@playwright/test"
import { assert } from "chai"
import {
Expand Down
1 change: 1 addition & 0 deletions src/tests/functional/navigation_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { test } from "@playwright/test"
import { assert } from "chai"
import {
Expand Down
53 changes: 53 additions & 0 deletions src/tests/functional/page_snapshot_tests.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
import "../helpers/trusted_type_setup"
import { test } from "@playwright/test"
import { assert } from "chai"

test.beforeEach(async ({ page }, workerInfo) => {
console.log(workerInfo.project.name)
await page.goto("/src/tests/fixtures/form.html")
await page.evaluate(() =>
window.Turbo.setCSPTrustedTypesPolicy({
createHTML: (s: string, response: Response) => {
response.headers.append("X-TRUSTED", "true")

return s.replace("Text", "Trusted")
},
createScript: (s) => s,
createScriptURL: (s) => s,
})
)
})

test.afterEach(async ({ page }) => {
await page.evaluate(() => window.Turbo.setCSPTrustedTypesPolicy(null))
})

const mockHTML = `
<html>
<head></head>
<body><div>Text</div></body>
</html>`

test("test fromResponse applies CSP Policies", async ({ page }) => {
const [text, header] = await page.evaluate((html) => {
const response = new Response(html)
const snapshot = window.Turbo.PageSnapshot.fromResponse(response, html)
return [snapshot.element.innerText, response.headers.get("X-TRUSTED")]
}, mockHTML)

assert.include(text, "Trusted")
assert.equal(header, "true")
})

test("test fromResponse without CSP policy doesn't change the response", async ({ page }) => {
await page.evaluate(() => window.Turbo.setCSPTrustedTypesPolicy(null))

const [text, header] = await page.evaluate((html) => {
const response = new Response(html)
const snapshot = window.Turbo.PageSnapshot.fromResponse(response, html)
return [snapshot.element.innerText, response.headers.get("X-TRUSTED")]
}, mockHTML)

assert.include(text, "Text")
assert.equal(header, undefined)
})
1 change: 1 addition & 0 deletions src/tests/functional/pausable_rendering_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { test } from "@playwright/test"
import { assert } from "chai"
import { nextBeat } from "../helpers/page"
Expand Down
1 change: 1 addition & 0 deletions src/tests/functional/pausable_requests_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { test } from "@playwright/test"
import { assert } from "chai"
import { nextBeat } from "../helpers/page"
Expand Down
1 change: 1 addition & 0 deletions src/tests/functional/preloader_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { test } from "@playwright/test"
import { assert } from "chai"
import { nextBeat } from "../helpers/page"
Expand Down
1 change: 1 addition & 0 deletions src/tests/functional/rendering_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { JSHandle, Page, test } from "@playwright/test"
import { assert } from "chai"
import {
Expand Down
1 change: 1 addition & 0 deletions src/tests/functional/scroll_restoration_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { test } from "@playwright/test"
import { assert } from "chai"
import { nextBeat, scrollPosition, scrollToSelector } from "../helpers/page"
Expand Down
1 change: 1 addition & 0 deletions src/tests/functional/stream_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { test } from "@playwright/test"
import { assert } from "chai"
import { nextBeat, nextEventNamed, readEventLogs } from "../helpers/page"
Expand Down
1 change: 1 addition & 0 deletions src/tests/functional/ujs_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { Page, test } from "@playwright/test"
import { assert } from "chai"
import { noNextEventOnTarget } from "../helpers/page"
Expand Down
1 change: 1 addition & 0 deletions src/tests/functional/visit_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { Page, test } from "@playwright/test"
import { assert } from "chai"
import { get } from "http"
Expand Down
1 change: 1 addition & 0 deletions src/tests/functional/visitable_tests.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "../helpers/trusted_type_setup"
import { test } from "@playwright/test"
import { assert } from "chai"
import { nextBody, pathname, visitAction } from "../helpers/page"
Expand Down
19 changes: 19 additions & 0 deletions src/tests/helpers/trusted_type_setup.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
import "../helpers/trusted_type_setup"
import { test } from "@playwright/test"

test.beforeEach(async ({ page }, workerInfo) => {
if (workerInfo.project.name === "chrome-with-trusted-types") {
await page.goto("/src/tests/fixtures/form.html")
await page.evaluate(() =>
window.Turbo.setCSPTrustedTypesPolicy({
createHTML: (s) => s,
createScript: (s) => s,
createScriptURL: (s) => s,
})
)
}
})

test.afterEach(async ({ page }) => {
await page.evaluate(() => window.Turbo.setCSPTrustedTypesPolicy(null))
})
2 changes: 1 addition & 1 deletion src/trusted_types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ interface TrustedTypesPolicyInterface {

let CSPTrustedTypesPolicy: CSPTrustedTypesPolicy | null = null

export function setCSPTrustedTypesPolicy(policy: CSPTrustedTypesPolicy) {
export function setCSPTrustedTypesPolicy(policy: CSPTrustedTypesPolicy | null) {
CSPTrustedTypesPolicy = policy
}

Expand Down