|
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | 16 |
|
| 17 | +import { spawnSync } from 'node:child_process'; |
17 | 18 | import { chromium, type Browser, type Page } from 'playwright'; |
| 19 | +import { TestSession } from '@salesforce/cli-plugins-testkit'; |
18 | 20 |
|
19 | 21 | /** |
20 | | - * Launches a headless (or headed, if HEADED env is set) browser, navigates to the component preview URL, |
21 | | - * and optionally sets the session cookie for authenticated preview. |
| 22 | + * Returns the access token for frontdoor sid authentication. Required for |
| 23 | + * Playwright testing. |
| 24 | + * |
| 25 | + * @param session - TestSession with a default scratch org. |
| 26 | + * @returns The session ID string. |
| 27 | + */ |
| 28 | +export function getAccessToken(session: TestSession): string { |
| 29 | + const scratchOrg = session.orgs.get('default'); |
| 30 | + const username = scratchOrg?.username ?? ''; |
| 31 | + const projectDir = session.project?.dir ?? ''; |
| 32 | + if (!username || !projectDir) { |
| 33 | + throw new Error('Session has no default scratch org username or project dir'); |
| 34 | + } |
| 35 | + const result = spawnSync('sf', ['org', 'display', 'user', '-o', username, '--json'], { |
| 36 | + cwd: projectDir, |
| 37 | + encoding: 'utf8', |
| 38 | + maxBuffer: 10 * 1024, |
| 39 | + }); |
| 40 | + if (result.status !== 0) { |
| 41 | + throw new Error(`sf org display user failed: ${result.stderr ?? result.error?.message ?? 'unknown'}`); |
| 42 | + } |
| 43 | + let displayUser: { result?: { accessToken?: string } }; |
| 44 | + try { |
| 45 | + displayUser = JSON.parse(result.stdout ?? '{}') as { result?: { accessToken?: string } }; |
| 46 | + } catch { |
| 47 | + throw new Error('sf org display user did not return valid JSON'); |
| 48 | + } |
| 49 | + const accessToken = displayUser.result?.accessToken ?? ''; |
| 50 | + if (!accessToken) { |
| 51 | + throw new Error('sf org display user result missing accessToken'); |
| 52 | + } |
| 53 | + return accessToken; |
| 54 | +} |
| 55 | + |
| 56 | +/** |
| 57 | + * Establishes a browser session by opening the Salesforce front-door URL with the access token. |
| 58 | + * The server redirects and sets session cookies (sid, sid_Client, etc.), so the LWR preview app |
| 59 | + * accepts the session. No password or form fill required. |
| 60 | + * |
| 61 | + * @param page - Playwright page. |
| 62 | + * @param previewOrigin - Org origin (e.g. from new URL(previewUrl).origin). |
| 63 | + * @param accessToken - Org access token (sid). |
| 64 | + */ |
| 65 | +async function establishSessionViaFrontDoor(page: Page, previewOrigin: string, accessToken: string): Promise<void> { |
| 66 | + const frontDoorUrl = `${previewOrigin}/secur/frontdoor.jsp?sid=${accessToken}`; |
| 67 | + await page.goto(frontDoorUrl, { waitUntil: 'commit' }); |
| 68 | +} |
| 69 | + |
| 70 | +/** |
| 71 | + * Launches a headless (or headed, if HEADED env is set) browser, establishes an authenticated |
| 72 | + * session (front-door URL first; falls back to form login if needed), then navigates to the preview URL. |
22 | 73 | * |
23 | 74 | * @param previewUrl - Full URL of the LWC component preview (e.g. from getPreviewURL). |
24 | | - * @param accessToken - Optional org access token; if set, adds a 'sid' cookie so the preview page is authenticated. |
| 75 | + * @param session - TestSession with default scratch org. |
| 76 | + * @param options - Optional. Use form login only (skip front-door) by setting useFormLogin: true. |
25 | 77 | * @returns Promise resolving to the Playwright browser and page; caller must close them when done. |
26 | 78 | */ |
27 | | -export async function getPreview( |
28 | | - previewUrl: string, |
29 | | - accessToken: string | undefined, |
30 | | -): Promise<{ browser: Browser; page: Page }> { |
| 79 | +export async function getPreview(previewUrl: string, session: TestSession): Promise<{ browser: Browser; page: Page }> { |
| 80 | + const accessToken = getAccessToken(session); |
31 | 81 | const previewOrigin = new URL(previewUrl).origin; |
32 | | - let browser: Browser | null = null; |
33 | | - let page: Page | null = null; |
34 | 82 | const headed = process.env.HEADED === 'true' || process.env.HEADED === '1'; |
35 | | - browser = await chromium.launch({ headless: !headed }); |
36 | | - page = await browser.newPage(); |
37 | | - if (accessToken) { |
38 | | - await page.context().addCookies([ |
39 | | - { |
40 | | - name: 'sid', |
41 | | - value: accessToken, |
42 | | - domain: new URL(previewOrigin).hostname, |
43 | | - path: '/', |
44 | | - expires: Math.floor(Date.now() / 1000) + 86_400, |
45 | | - }, |
46 | | - ]); |
47 | | - } |
| 83 | + const browser = await chromium.launch({ headless: !headed }); |
| 84 | + const page = await browser.newPage(); |
| 85 | + |
| 86 | + await establishSessionViaFrontDoor(page, previewOrigin, accessToken); |
48 | 87 | await page.goto(previewUrl, { waitUntil: 'load' }); |
49 | 88 | return new Promise((r) => r({ browser, page })); |
50 | 89 | } |
0 commit comments