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
4 changes: 4 additions & 0 deletions tests/e2e/pages/modules/flex-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class FlexRender {
readonly inlineEmbed: Locator
readonly inlineRuntime: Locator
readonly ssrEmbed: Locator
readonly ssrHydrated: Locator
readonly embed: (mode: DeliveryMode) => Locator

constructor(readonly root: Page | FrameLocator) {
Expand All @@ -19,6 +20,9 @@ export class FlexRender {
.locator('div[data-flex-inline] + script[src*="flex-client"]')
.describe('flex inline runtime script')
this.ssrEmbed = root.locator('div.ceros-block__flex-ssr').describe('flex server-rendered embed')
this.ssrHydrated = root
.locator('div.ceros-block__flex-ssr[data-flex-router-ready] > [data-flex-ready="true"]')
.describe('flex server-rendered embed, hydrated')
this.embed = (mode: DeliveryMode) =>
({ iframe: this.iframeEmbed, inline: this.inlineEmbed, ssr: this.ssrEmbed })[mode]
}
Expand Down
20 changes: 20 additions & 0 deletions tests/e2e/pages/published-post.page.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { Locator, Page } from '@playwright/test'
import { WP_ROUTES } from '@constants/wordpress-constants'
import { FlexRender } from '@pages/modules/flex-render'
import type { ImportMap } from '@utils/ceros-embed'

/** A published post on the front end — where the render cascade shows its work. */
export class PublishedPostPage {
readonly flex: FlexRender
readonly iframeEmbed: Locator
readonly importMaps: Locator
readonly missingExperience: Locator
readonly missingExperienceHeading: Locator

Expand All @@ -14,6 +16,7 @@ export class PublishedPostPage {
this.iframeEmbed = page
.locator('div[data-ceros-experience], div[data-aspectRatio] iframe.ceros-experience')
.describe('iframe embed marker')
this.importMaps = page.locator('script[type="importmap"]').describe('import map')
this.missingExperience = page
.locator('div.ceros-missing-experience')
.describe('missing experience block')
Expand All @@ -25,4 +28,21 @@ export class PublishedPostPage {
async open(postId: number): Promise<void> {
await this.page.goto(WP_ROUTES.permalink(postId))
}

async importMap(): Promise<ImportMap> {
return JSON.parse(String(await this.importMaps.first().textContent())) as ImportMap
}

/** The module scripts and module preloads that come before the import map. */
async modulesBeforeImportMap(): Promise<string[]> {
return this.page.evaluate(() => {
const map = document.querySelector('script[type="importmap"]')
if (!map) return ['no import map']
return Array.from(
document.querySelectorAll('script[type="module"], link[rel="modulepreload"]'),
)
.filter((m) => !(map.compareDocumentPosition(m) & Node.DOCUMENT_POSITION_FOLLOWING))
.map((m) => m.outerHTML)
})
}
}
41 changes: 41 additions & 0 deletions tests/e2e/tests/flex-ssr-import-map.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { expect, test } from '@fixtures/fixtures'
import { TIMEOUTS } from '@constants/timeouts'
import { PublishedPostPage } from '@pages/published-post.page'
import { resolvedFlexBlock } from '@utils/block-serializer'
import { expectImportMapCarries, readManifestImportMap } from '@utils/ceros-embed'
import { cerosFlexExperienceUrl, cerosFlexManifestUrl, experienceSlug } from '@utils/env-utils'
import { TAGS } from '@utils/test-tags'

const manifestUrl = cerosFlexManifestUrl()

/** WordPress prints one import map for the page; the SSR runtime's imports join it. */
test.describe('Flex SSR import map', { tag: [TAGS.cerosBlock, TAGS.rendered, TAGS.flex] }, () => {
// A real reader is anonymous; render the published page logged out.
test.use({ storageState: { cookies: [], origins: [] } })

test.use({
postOptions: {
title: 'flex ssr import map',
blocks: [resolvedFlexBlock(experienceSlug(cerosFlexExperienceUrl()), manifestUrl, 'ssr')],
status: 'publish',
},
})

test("the import map leads the modules and carries the experience's imports", async ({
page,
post,
request,
}) => {
const pageErrors: Error[] = []
page.on('pageerror', (error) => pageErrors.push(error))
const manifestMap = await readManifestImportMap(request, manifestUrl)

const publishedPost = new PublishedPostPage(page)
await publishedPost.open(post.id)

// The runtime loads from the Ceros CDN, slower on a cold edge after a deploy.
await expect(publishedPost.flex.ssrHydrated).toHaveCount(1, { timeout: TIMEOUTS.MEDIUM })
await expectImportMapCarries(publishedPost, manifestMap)
expect(pageErrors).toEqual([])
})
})
37 changes: 36 additions & 1 deletion tests/e2e/utils/ceros-embed.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, type Locator } from '@playwright/test'
import { expect, type APIRequestContext, type Locator } from '@playwright/test'
import type { RequestUtils } from '@wordpress/e2e-test-utils-playwright'
import type { PublishedPostPage } from '@pages/published-post.page'
import { readStoredBlockAttributes } from '@utils/wp-rest-client'

/** The two iframe embed sizes the block stores as `selectedOption`. */
Expand All @@ -8,6 +9,9 @@ export type EmbedSize = 'full' | 'scroll'
/** How a Flex experience is delivered, stored as `deliveryMode`. */
export type DeliveryMode = 'iframe' | 'inline' | 'ssr'

/** A page or manifest import map. */
export type ImportMap = { imports: Record<string, string>; integrity?: Record<string, string> }

/** The wrapper every stored Studio embed code keeps, even after filtering strips its iframe and script. */
const STUDIO_EMBED_WRAPPER = /data-aspectratio=/i

Expand Down Expand Up @@ -91,3 +95,34 @@ export async function expectStoredFlexAttributes(
)
expect(String(stored.inlineEmbedCode ?? '')).toContain(FLEX_INLINE_MARKER)
}

/**
* Read the import map from a Flex manifest. Fails when it has no imports or hashes,
* since checks against it would then prove nothing.
*/
export async function readManifestImportMap(
request: APIRequestContext,
manifestUrl: string,
): Promise<ImportMap> {
const response = await request.get(manifestUrl)
await expect(response).toBeOK()
const { importMap } = (await response.json()) as { importMap?: ImportMap }
expect(Object.keys(importMap?.imports ?? {}), 'manifest import map entries').not.toEqual([])
expect(importMap?.integrity, 'manifest import map integrity').toBeTruthy()
return importMap as ImportMap
}

/** Assert the page prints one import map, ahead of every module, carrying the manifest's imports and hashes. */
export async function expectImportMapCarries(
publishedPost: PublishedPostPage,
manifestMap: ImportMap,
): Promise<void> {
await expect(publishedPost.importMaps).toHaveCount(1)
expect(await publishedPost.modulesBeforeImportMap()).toEqual([])
const map = await publishedPost.importMap()
const specifiers = Object.keys(manifestMap.imports)
expect(Object.keys(map.imports)).toEqual(expect.arrayContaining(specifiers))
for (const specifier of specifiers) {
expect(map.integrity?.[map.imports[specifier]], specifier).toBeTruthy()
}
}
Loading