Repository navigation
Fix - Block editor never loads the theme fonts and ignores the primary color #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
25fa464
Fix - Block editor ignores the Customizer primary color and never loa…
rajatgautam755421 4238015
Style plain buttons in the editor like the front end, and exercise a …
rajatgautam755421 ffd59eb
Give the test login helper time for a slow dashboard
rajatgautam755421 7af4b00
Output only valid colors, sizes and fonts in the editor CSS
rajatgautam755421 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| import { test, expect, type Frame, type Page } from "@playwright/test"; | ||
| import { login, requireAdmin, restNonce } from "../../utils/wp"; | ||
|
|
||
| /** | ||
| * @area block-editor | ||
| * @tier fresh | ||
| * @guards accelerate-pro#47 | ||
| * @source accelerate-pro#47 (reported by iamprazol) | ||
| * @why The block editor never used the Customizer: it loaded a static stylesheet | ||
| * and a hardcoded Roboto link on a hook whose styles never reach the editor | ||
| * iframe, so no theme font loaded in the canvas, and the title, form fields, | ||
| * buttons and the Customizer primary color differed from the front end. | ||
| * Saves a custom primary color through the Customizer itself (restored | ||
| * afterwards), then checks on a post and a page | ||
| * that the canvas carries the front end's fonts stylesheet, that the theme's | ||
| * editor styles reach the iframe the supported way, and that each element | ||
| * computes the same font, size, weight, color and background in the editor | ||
| * as on the published page. Reads the stylesheet links, so it needs no | ||
| * network access to Google. | ||
| */ | ||
| const settings: Record<string, string> = { | ||
| accelerate_primary_color: "#d63384", | ||
| }; | ||
|
|
||
| const content = [ | ||
| '<!-- wp:heading --><h2 class="wp-block-heading">TGQA heading</h2><!-- /wp:heading -->', | ||
| '<!-- wp:paragraph --><p>TGQA text with <a href="https://example.com">a link</a>.</p><!-- /wp:paragraph -->', | ||
| '<!-- wp:list --><ul class="wp-block-list"><!-- wp:list-item --><li>TGQA list item</li><!-- /wp:list-item --></ul><!-- /wp:list -->', | ||
| '<!-- wp:freeform --><blockquote><p>TGQA classic quote</p></blockquote><p><input type="submit" value="Go" /> <button type="button">Plain button</button></p><!-- /wp:freeform -->', | ||
| '<!-- wp:search {"label":"Search","buttonText":"Search"} /-->', | ||
| ].join("\n"); | ||
|
|
||
| const targets: Record<string, string> = { | ||
| heading: "h2.wp-block-heading", | ||
| text: "p", | ||
| link: "p a", | ||
| list: "li", | ||
| "classic quote": "blockquote:not(.wp-block-quote)", | ||
| submit: "input[type=submit]", | ||
| button: "p button", | ||
| "search field": ".wp-block-search__input", | ||
| "search button": ".wp-block-search__button", | ||
| }; | ||
|
|
||
| async function styles(root: Page | Frame, scope: string, title: string) { | ||
| return root.locator(scope).first().evaluate( | ||
| (el, { targets, title }) => { | ||
| const read = (node: Element | null) => { | ||
| if (!node) return null; | ||
| const c = getComputedStyle(node); | ||
| return `${c.fontFamily.split(",")[0].replace(/["']/g, "")} ${c.fontSize} ${c.fontWeight} ${c.color} on ${c.backgroundColor}`; | ||
| }; | ||
| const out: Record<string, string | null> = { title: read(document.querySelector(title)) }; | ||
| for (const [name, selector] of Object.entries(targets)) out[name] = read(el.querySelector(selector)); | ||
| return out; | ||
| }, | ||
| { targets, title }, | ||
| ); | ||
| } | ||
|
|
||
| /** Set Customizer values through the Customizer's own API and publish them; returns the previous values. */ | ||
| async function saveCustomizer(page: Page, values: Record<string, string>): Promise<Record<string, string>> { | ||
| await page.goto("/wp-admin/customize.php", { waitUntil: "domcontentloaded" }); | ||
| await page.waitForFunction(() => (window as any).wp?.customize?.state?.("activated")?.get(), null, { timeout: 90_000 }); | ||
| return page.evaluate(async (values) => { | ||
| const api = (window as any).wp.customize; | ||
| const previous: Record<string, string> = {}; | ||
| for (const [key, value] of Object.entries(values)) { | ||
| const setting = api(`accelerate[${key}]`); | ||
| if (!setting) throw new Error(`Customizer setting accelerate[${key}] is not registered`); | ||
| previous[key] = setting.get(); | ||
| setting.set(value); | ||
| } | ||
| await new Promise((resolve, reject) => api.previewer.save().done(resolve).fail(reject)); | ||
| return previous; | ||
| }, values); | ||
| } | ||
|
|
||
| async function createPost(page: Page, type: "posts" | "pages", title: string): Promise<{ id: number; link: string }> { | ||
| const res = await page.request.post(`/?rest_route=/wp/v2/${type}`, { | ||
| headers: { "X-WP-Nonce": await restNonce(page) }, | ||
| data: { title, content, status: "publish" }, | ||
| }); | ||
| expect(res.ok(), `creating the test ${type} failed: HTTP ${res.status()}`).toBeTruthy(); | ||
| const body = await res.json(); | ||
| return { id: body.id, link: body.link }; | ||
| } | ||
|
|
||
| // POST with a method override: some hosts answer a bare DELETE with 405. | ||
| async function deletePost(page: Page, type: "posts" | "pages", id: number): Promise<void> { | ||
| const res = await page.request.post(`/?rest_route=/wp/v2/${type}/${id}&force=true`, { | ||
| headers: { "X-WP-Nonce": await restNonce(page), "X-HTTP-Method-Override": "DELETE" }, | ||
| }); | ||
| expect(res.ok(), `deleting test ${type} ${id} failed: HTTP ${res.status()}`).toBeTruthy(); | ||
| } | ||
|
|
||
| const fontsHref = (href: string | null) => (href ?? "").replace(/^https?:/, "").replace(/&(amp;|#038;)?ver=[^&]*/, ""); | ||
|
|
||
| test("the block editor uses the front end's fonts, typography and colors @block-editor @fresh", async ({ | ||
| page, | ||
| }, testInfo) => { | ||
| requireAdmin(testInfo); | ||
| test.setTimeout(420_000); | ||
| const iframeWarnings: string[] = []; | ||
| page.on("console", (message) => { | ||
| if (/accelerate.*added to the iframe incorrectly/.test(message.text())) iframeWarnings.push(message.text()); | ||
| }); | ||
| await login(page); | ||
| let previous: Record<string, string> | null = null; | ||
| const created: { type: "posts" | "pages"; id: number }[] = []; | ||
| try { | ||
| previous = await saveCustomizer(page, settings); | ||
|
|
||
| for (const type of ["posts", "pages"] as const) { | ||
| const item = await createPost(page, type, `TGQA editor styles ${type}`); | ||
| created.push({ type, id: item.id }); | ||
|
|
||
| await page.goto(item.link); | ||
| const front = await styles(page, ".entry-content", ".entry-title"); | ||
| // No link when only standard (system) fonts are chosen. | ||
| const frontFonts = await page.locator("#accelerate_googlefonts-css").evaluateAll((links) => links[0]?.getAttribute("href") ?? null); | ||
|
|
||
| await page.goto(`/wp-admin/post.php?post=${item.id}&action=edit`, { waitUntil: "domcontentloaded" }); | ||
| const canvas = page.frameLocator('iframe[name="editor-canvas"]'); | ||
| await expect(canvas.locator(".editor-styles-wrapper .wp-block-search__input")).toBeVisible({ timeout: 90_000 }); | ||
| const frame = page.frame({ name: "editor-canvas" })!; | ||
|
|
||
| await expect(canvas.locator("#accelerate-block-editor-styles-css"), "the theme's editor styles must reach the canvas").toBeAttached({ timeout: 15_000 }); | ||
| const editorFonts = await canvas.locator("#accelerate-editor-googlefonts-css").evaluateAll((links) => links[0]?.getAttribute("href") ?? null); | ||
| expect.soft(editorFonts && fontsHref(editorFonts), `${type}: the editor canvas must load the front end's fonts stylesheet`).toBe(frontFonts && fontsHref(frontFonts)); | ||
|
|
||
| const editor = await styles(frame, ".editor-styles-wrapper", ".editor-post-title__input"); | ||
| for (const name of Object.keys(front)) { | ||
| expect(front[name], `${type}: ${name} is missing on the front end`).not.toBeNull(); | ||
| expect.soft(editor[name], `${type}: ${name} (font size weight color on background) differs from the front end`).toBe(front[name]); | ||
| } | ||
| } | ||
| expect(iframeWarnings).toEqual([]); | ||
| } finally { | ||
| for (const { type, id } of created) await deletePost(page, type, id); | ||
| if (previous) await saveCustomizer(page, previous); | ||
| } | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import { expect, type Page, type TestInfo } from "@playwright/test"; | ||
|
|
||
| /** | ||
| * Shared helpers for specs that need wp-admin. Specs that change site state | ||
| * put it back in a finally block. | ||
| */ | ||
|
|
||
| export const adminUser = process.env.TGQA_ADMIN_USER ?? ""; | ||
| export const adminPass = process.env.TGQA_ADMIN_PASS ?? ""; | ||
|
|
||
| /** Skip, with a reason in the report, when no admin credentials were supplied. */ | ||
| export function requireAdmin(testInfo: TestInfo): void { | ||
| testInfo.skip( | ||
| !adminUser || !adminPass, | ||
| "needs TGQA_ADMIN_USER / TGQA_ADMIN_PASS in .themegrill-qa/.env.local", | ||
| ); | ||
| } | ||
|
|
||
| export async function login(page: Page): Promise<void> { | ||
| await page.goto("/wp-login.php"); | ||
| await page.locator("#user_login").fill(adminUser); | ||
| await page.locator("#user_pass").fill(adminPass); | ||
| await page.locator("#wp-submit").click(); | ||
| // The dashboard can take several seconds to load on a busy site. | ||
| await expect(page.locator("#wpadminbar")).toBeVisible({ timeout: 30_000 }); | ||
| } | ||
|
|
||
| /** A REST nonce for the logged-in session, so page.request can call wp/v2. */ | ||
| export async function restNonce(page: Page): Promise<string> { | ||
| const res = await page.request.get("/wp-admin/admin-ajax.php?action=rest-nonce"); | ||
| expect(res.ok(), "could not get a REST nonce — is the admin logged in?").toBeTruthy(); | ||
| return (await res.text()).trim(); | ||
| } | ||
|
|
||
| /** Create a published page through the REST API and return its id and link. */ | ||
| export async function createPage( | ||
| page: Page, | ||
| title: string, | ||
| content: string, | ||
| ): Promise<{ id: number; link: string }> { | ||
| const nonce = await restNonce(page); | ||
| const res = await page.request.post("/?rest_route=/wp/v2/pages", { | ||
| headers: { "X-WP-Nonce": nonce }, | ||
| data: { title, content, status: "publish" }, | ||
| }); | ||
| expect(res.ok(), `creating the test page failed: HTTP ${res.status()}`).toBeTruthy(); | ||
| const body = await res.json(); | ||
| return { id: body.id, link: body.link }; | ||
| } | ||
|
|
||
| // POST with a method override rather than DELETE: some hosts (this Local site's | ||
| // nginx among them) answer a bare DELETE with 405 before WordPress sees it. | ||
| export async function deletePage(page: Page, id: number): Promise<void> { | ||
| const nonce = await restNonce(page); | ||
| const res = await page.request.post(`/?rest_route=/wp/v2/pages/${id}&force=true`, { | ||
| headers: { "X-WP-Nonce": nonce, "X-HTTP-Method-Override": "DELETE" }, | ||
| }); | ||
| expect(res.ok(), `deleting test page ${id} failed: HTTP ${res.status()}`).toBeTruthy(); | ||
| } | ||
|
|
||
| /** Remove a widget through the REST API (method override, as for pages above). */ | ||
| export async function removeWidget(page: Page, id: string): Promise<void> { | ||
| const res = await page.request.post(`/?rest_route=/wp/v2/widgets/${id}&force=true`, { | ||
| headers: { "X-WP-Nonce": await restNonce(page), "X-HTTP-Method-Override": "DELETE" }, | ||
| }); | ||
| expect(res.ok(), `removing test widget ${id} failed: HTTP ${res.status()}`).toBeTruthy(); | ||
| } | ||
|
|
||
| /** | ||
| * Add a classic (legacy) widget to a sidebar through the REST API, the way the block Widgets | ||
| * editor does: encode the form, create the widget, then place it (over HTTP a new widget | ||
| * lands in Inactive Widgets). `fields` are the widget form's field names and values. | ||
| */ | ||
| export async function addLegacyWidget( | ||
| page: Page, | ||
| idBase: string, | ||
| sidebar: string, | ||
| fields: Record<string, string | number>, | ||
| ): Promise<string> { | ||
| const headers = { "X-WP-Nonce": await restNonce(page) }; | ||
| const form = Object.entries(fields) | ||
| .map(([k, v]) => `widget-${idBase}[1][${k}]=${encodeURIComponent(String(v))}`) | ||
| .join("&"); | ||
| const encoded = await page.request.post(`/?rest_route=/wp/v2/widget-types/${idBase}/encode`, { headers, data: { form_data: form } }); | ||
| expect(encoded.ok(), `encoding the ${idBase} widget failed: HTTP ${encoded.status()}`).toBeTruthy(); | ||
| const created = await page.request.post("/?rest_route=/wp/v2/widgets", { | ||
| headers, | ||
| data: { id_base: idBase, sidebar, instance: (await encoded.json()).instance }, | ||
| }); | ||
| expect(created.ok(), `creating the ${idBase} widget failed: HTTP ${created.status()}`).toBeTruthy(); | ||
| const id: string = (await created.json()).id; | ||
| try { | ||
| const placed = await page.request.post(`/?rest_route=/wp/v2/widgets/${id}`, { | ||
| headers: { ...headers, "X-HTTP-Method-Override": "PUT" }, | ||
| data: { sidebar }, | ||
| }); | ||
| expect(placed.ok(), `placing the ${idBase} widget failed: HTTP ${placed.status()}`).toBeTruthy(); | ||
| } catch (error) { | ||
| // The caller never gets the id, so its cleanup cannot remove this widget. | ||
| // A cleanup failure must not hide the placement error, which is the real cause. | ||
| await removeWidget(page, id).catch(() => {}); | ||
| throw error; | ||
| } | ||
| return id; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.