Skip to content

Commit

Permalink
feat(e2e): Tests for Keyless mode (#5046)
Browse files Browse the repository at this point in the history
  • Loading branch information
panteliselef authored Jan 31, 2025
1 parent e6df88a commit a75a1f7
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .changeset/strong-apricots-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
5 changes: 4 additions & 1 deletion integration/models/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const application = (
appDirName: string,
serverUrl: string | undefined,
) => {
const { name, scripts, envWriter } = config;
const { name, scripts, envWriter, copyKeylessToEnv } = config;
const logger = createLogger({ prefix: `${appDirName}` });
const state = { completedSetup: false, serverUrl: '', env: {} as EnvironmentConfig };
const cleanupFns: { (): unknown }[] = [];
Expand All @@ -36,6 +36,9 @@ export const application = (
state.env = env;
return envWriter(appDirPath, env);
},
keylessToEnv: async () => {
return copyKeylessToEnv(appDirPath);
},
setup: async (opts?: { strategy?: 'ci' | 'i' | 'copy'; force?: boolean }) => {
const { force } = opts || {};
const nodeModulesExist = await fs.pathExists(path.resolve(appDirPath, 'node_modules'));
Expand Down
24 changes: 24 additions & 0 deletions integration/models/applicationConfig.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as path from 'node:path';

import type { AccountlessApplication } from '@clerk/backend';

import { constants } from '../constants';
import { createLogger, fs } from '../scripts';
import { application } from './application';
Expand Down Expand Up @@ -130,6 +132,28 @@ export const applicationConfig = () => {
get scripts() {
return scripts;
},
get copyKeylessToEnv() {
const writer = async (appDir: string) => {
const CONFIG_PATH = path.join(appDir, '.clerk', '.tmp', 'keyless.json');
try {
const fileAsString = await fs.readFile(CONFIG_PATH, { encoding: 'utf-8' });
const maybeAccountlessApplication: AccountlessApplication = JSON.parse(fileAsString);
if (maybeAccountlessApplication.publishableKey) {
await fs.writeFile(
path.join(appDir, '.env'),
`${envFormatters.public('CLERK_PUBLISHABLE_KEY')}=${maybeAccountlessApplication.publishableKey}\n` +
`${envFormatters.private('CLERK_SECRET_KEY')}=${maybeAccountlessApplication.secretKey}`,
{
flag: 'a',
},
);
}
} catch (e) {
throw new Error('unable to copy keys from .clerk/', e);
}
};
return writer;
},
get envWriter() {
const defaultWriter = async (appDir: string, env: EnvironmentConfig) => {
// Create env files
Expand Down
3 changes: 3 additions & 0 deletions integration/presets/envs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const base = environmentConfig()
.setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up')
.setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js');

const withKeyless = base.clone().setEnvVariable('public', 'CLERK_ENABLE_KEYLESS', true);

const withEmailCodes = base
.clone()
.setId('withEmailCodes')
Expand Down Expand Up @@ -132,6 +134,7 @@ const withSignInOrUpwithRestrictedModeFlow = withEmailCodes

export const envs = {
base,
withKeyless,
withEmailCodes,
withEmailCodes_destroy_client,
withEmailLinks,
Expand Down
2 changes: 2 additions & 0 deletions integration/testUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Application } from '../models/application';
import { createAppPageObject } from './appPageObject';
import { createEmailService } from './emailService';
import { createInvitationService } from './invitationsService';
import { createKeylessPopoverPageObject } from './keylessPopoverPageObject';
import { createOrganizationSwitcherComponentPageObject } from './organizationSwitcherPageObject';
import type { EnchancedPage, TestArgs } from './signInPageObject';
import { createSignInComponentPageObject } from './signInPageObject';
Expand Down Expand Up @@ -88,6 +89,7 @@ export const createTestUtils = <
const testArgs = { page, context, browser };

const pageObjects = {
keylessPopover: createKeylessPopoverPageObject(testArgs),
signUp: createSignUpComponentPageObject(testArgs),
signIn: createSignInComponentPageObject(testArgs),
userProfile: createUserProfileComponentPageObject(testArgs),
Expand Down
33 changes: 33 additions & 0 deletions integration/testUtils/keylessPopoverPageObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Browser, BrowserContext } from '@playwright/test';

import type { createAppPageObject } from './appPageObject';

export type EnchancedPage = ReturnType<typeof createAppPageObject>;
export type TestArgs = { page: EnchancedPage; context: BrowserContext; browser: Browser };

export const createKeylessPopoverPageObject = (testArgs: TestArgs) => {
const { page } = testArgs;
// TODO: Is this the ID we really want ?
const elementId = '#--clerk-keyless-prompt-button';
const self = {
waitForMounted: () => page.waitForSelector(elementId, { state: 'attached' }),
waitForUnmounted: () => page.waitForSelector(elementId, { state: 'detached' }),
isExpanded: () =>
page
.locator(elementId)
.getAttribute('aria-expanded')
.then(val => val === 'true'),
toggle: () => page.locator(elementId).click(),

promptsToClaim: () => {
return page.getByRole('link', { name: /^claim application$/i });
},
promptToUseClaimedKeys: () => {
return page.getByRole('link', { name: /^get api keys$/i });
},
promptToDismiss: () => {
return page.getByRole('button', { name: /^dismiss$/i });
},
};
return self;
};
119 changes: 119 additions & 0 deletions integration/tests/next-quickstart-keyless.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';

import type { Application } from '../models/application';
import { appConfigs } from '../presets';
import { createTestUtils } from '../testUtils';

const commonSetup = appConfigs.next.appRouterQuickstart.clone();

const mockClaimedInstanceEnvironmentCall = async (page: Page) => {
await page.route('*/**/v1/environment*', async route => {
const response = await route.fetch();
const json = await response.json();
const newJson = {
...json,
auth_config: {
...json.auth_config,
claimed_at: Date.now(),
},
};
await route.fulfill({ response, json: newJson });
});
};

test.describe('Keyless mode @quickstart', () => {
test.describe.configure({ mode: 'serial' });
let app: Application;

test.beforeAll(async () => {
app = await commonSetup.commit();
await app.setup();
await app.withEnv(appConfigs.envs.withKeyless);
await app.dev();
});

test.afterAll(async () => {
await app.teardown();
});

test('Toggle collapse popover and claim.', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.page.goToAppHome();
await u.page.waitForClerkJsLoaded();
await u.po.expect.toBeSignedOut();

await u.po.keylessPopover.waitForMounted();

expect(await u.po.keylessPopover.isExpanded()).toBe(false);
await u.po.keylessPopover.toggle();
expect(await u.po.keylessPopover.isExpanded()).toBe(true);

const claim = await u.po.keylessPopover.promptsToClaim();

const [newPage] = await Promise.all([context.waitForEvent('page'), claim.click()]);

await newPage.waitForLoadState();
await newPage.waitForURL(url => {
const urlToReturnTo = 'https://dashboard.clerk.com/apps/claim?token=';
return (
url.pathname === '/apps/claim/sign-in' &&
url.searchParams.get('sign_in_force_redirect_url')?.startsWith(urlToReturnTo) &&
url.searchParams.get('sign_up_force_redirect_url')?.startsWith(urlToReturnTo)
);
});
});

test('Lands on claimed application with missing explicit keys, expanded by default, click to get keys from dashboard.', async ({
page,
context,
}) => {
await mockClaimedInstanceEnvironmentCall(page);
const u = createTestUtils({ app, page, context });
await u.page.goToAppHome();
await u.page.waitForClerkJsLoaded();

await u.po.keylessPopover.waitForMounted();
expect(await u.po.keylessPopover.isExpanded()).toBe(true);
await expect(u.po.keylessPopover.promptToUseClaimedKeys()).toBeVisible();

const [newPage] = await Promise.all([
context.waitForEvent('page'),
u.po.keylessPopover.promptToUseClaimedKeys().click(),
]);

await newPage.waitForLoadState();
await newPage.waitForURL(url =>
url.href.startsWith(
'https://dashboard.clerk.com/sign-in?redirect_url=https%3A%2F%2Fdashboard.clerk.com%2Fapps%2Fapp_',
),
);
});

test('Claimed application with keys inside .env, on dismiss, keyless prompt is removed.', async ({
page,
context,
}) => {
await mockClaimedInstanceEnvironmentCall(page);
const u = createTestUtils({ app, page, context });
await u.page.goToAppHome();

await u.po.keylessPopover.waitForMounted();
await expect(await u.po.keylessPopover.promptToUseClaimedKeys()).toBeVisible();

/**
* Copy keys from `.clerk/.tmp/keyless.json to `.env`
*/
await app.keylessToEnv();
/**
* wait a bit for the server to load the new env file
*/
await page.waitForTimeout(5_000);

await page.reload();
await u.po.keylessPopover.waitForMounted();
await u.po.keylessPopover.promptToDismiss().click();

await u.po.keylessPopover.waitForUnmounted();
});
});

0 comments on commit a75a1f7

Please sign in to comment.