WIP: improve font caching and partial fix on test

This commit is contained in:
Matthew Raymer
2025-07-27 01:56:42 +00:00
parent c628c78565
commit e04c9f3626
16 changed files with 338 additions and 87 deletions

View File

@@ -225,8 +225,8 @@ export function setupContentSecurityPolicy(customScheme: string): void {
...details.responseHeaders, ...details.responseHeaders,
'Content-Security-Policy': [ 'Content-Security-Policy': [
electronIsDev electronIsDev
? `default-src ${customScheme}://* 'unsafe-inline' devtools://* 'unsafe-eval' data: https: http:; style-src ${customScheme}://* 'unsafe-inline' https://fonts.googleapis.com; font-src ${customScheme}://* https://fonts.gstatic.com data:` ? `default-src ${customScheme}://* 'unsafe-inline' devtools://* 'unsafe-eval' data: https: http:; style-src ${customScheme}://* 'unsafe-inline'; font-src ${customScheme}://* data:`
: `default-src ${customScheme}://* 'unsafe-inline' data: https:; style-src ${customScheme}://* 'unsafe-inline' https://fonts.googleapis.com; font-src ${customScheme}://* https://fonts.gstatic.com data:`, : `default-src ${customScheme}://* 'unsafe-inline' data: https:; style-src ${customScheme}://* 'unsafe-inline'; font-src ${customScheme}://* data:`,
], ],
}, },
}); });

117
scripts/download-fonts.sh Executable file
View File

@@ -0,0 +1,117 @@
#!/bin/bash
# Download Work Sans font files locally
# This script downloads the Work Sans font family and creates local CSS
FONT_DIR="src/assets/fonts"
CSS_FILE="src/assets/styles/fonts.css"
# Create fonts directory
mkdir -p "$FONT_DIR"
# Download Work Sans font files
echo "Downloading Work Sans font files..."
# Regular weights (300, 400, 500, 600, 700)
curl -o "$FONT_DIR/WorkSans-Light.ttf" "https://fonts.gstatic.com/s/worksans/v23/QGY_z_wNahGAdqQ43RhVcIgYT2Xz5u32KxfXNig.ttf"
curl -o "$FONT_DIR/WorkSans-Regular.ttf" "https://fonts.gstatic.com/s/worksans/v23/QGY_z_wNahGAdqQ43RhVcIgYT2Xz5u32K0nXNig.ttf"
curl -o "$FONT_DIR/WorkSans-Medium.ttf" "https://fonts.gstatic.com/s/worksans/v23/QGY_z_wNahGAdqQ43RhVcIgYT2Xz5u32K3vXNig.ttf"
curl -o "$FONT_DIR/WorkSans-SemiBold.ttf" "https://fonts.gstatic.com/s/worksans/v23/QGY_z_wNahGAdqQ43RhVcIgYT2Xz5u32K5fQNig.ttf"
curl -o "$FONT_DIR/WorkSans-Bold.ttf" "https://fonts.gstatic.com/s/worksans/v23/QGY_z_wNahGAdqQ43RhVcIgYT2Xz5u32K67QNig.ttf"
# Italic weights (300, 400, 500, 600, 700)
curl -o "$FONT_DIR/WorkSans-LightItalic.ttf" "https://fonts.gstatic.com/s/worksans/v23/QGY9z_wNahGAdqQ43Rh_ebrnlwyYfEPxPoGUgGsJow.ttf"
curl -o "$FONT_DIR/WorkSans-Italic.ttf" "https://fonts.gstatic.com/s/worksans/v23/QGY9z_wNahGAdqQ43Rh_ebrnlwyYfEPxPoGU3msJow.ttf"
curl -o "$FONT_DIR/WorkSans-MediumItalic.ttf" "https://fonts.gstatic.com/s/worksans/v23/QGY9z_wNahGAdqQ43Rh_ebrnlwyYfEPxPoGU7GsJow.ttf"
curl -o "$FONT_DIR/WorkSans-SemiBoldItalic.ttf" "https://fonts.gstatic.com/s/worksans/v23/QGY9z_wNahGAdqQ43Rh_ebrnlwyYfEPxPoGUAGwJow.ttf"
curl -o "$FONT_DIR/WorkSans-BoldItalic.ttf" "https://fonts.gstatic.com/s/worksans/v23/QGY9z_wNahGAdqQ43Rh_ebrnlwyYfEPxPoGUOWwJow.ttf"
echo "Font files downloaded to $FONT_DIR"
# Create local CSS file
cat > "$CSS_FILE" << 'EOF'
/* Work Sans font family - locally hosted */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('../fonts/WorkSans-Light.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../fonts/WorkSans-Regular.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('../fonts/WorkSans-Medium.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('../fonts/WorkSans-SemiBold.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('../fonts/WorkSans-Bold.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url('../fonts/WorkSans-LightItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('../fonts/WorkSans-Italic.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url('../fonts/WorkSans-MediumItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url('../fonts/WorkSans-SemiBoldItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('../fonts/WorkSans-BoldItalic.ttf') format('truetype');
}
EOF
echo "Local font CSS created at $CSS_FILE"
echo "Don't forget to update tailwind.css to import this file instead of Google Fonts!"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,81 @@
/* Work Sans font family - locally hosted */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('../fonts/WorkSans-Light.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../fonts/WorkSans-Regular.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('../fonts/WorkSans-Medium.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('../fonts/WorkSans-SemiBold.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('../fonts/WorkSans-Bold.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url('../fonts/WorkSans-LightItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('../fonts/WorkSans-Italic.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url('../fonts/WorkSans-MediumItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url('../fonts/WorkSans-SemiBoldItalic.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('../fonts/WorkSans-BoldItalic.ttf') format('truetype');
}

View File

@@ -1,4 +1,4 @@
@import url('https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap'); @import './fonts.css';
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;

View File

@@ -1,5 +1,5 @@
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { switchToUser, getTestUserData } from './testUtils'; import { switchToUser, getTestUserData, importUserFromAccount } from './testUtils';
test('New offers for another user', async ({ page }) => { test('New offers for another user', async ({ page }) => {
await page.goto('./'); await page.goto('./');
@@ -15,30 +15,9 @@ test('New offers for another user', async ({ page }) => {
await page.getByTestId('closeOnboardingAndFinish').click(); await page.getByTestId('closeOnboardingAndFinish').click();
await expect(page.getByTestId('newDirectOffersActivityNumber')).toBeHidden(); await expect(page.getByTestId('newDirectOffersActivityNumber')).toBeHidden();
// Navigate to AccountViewView to use the Identity Switcher // Become User Zero
await page.goto('./account'); await importUserFromAccount(page, "00");
// Click "Show Advanced Settings" to reveal the identity switcher
await page.getByTestId('advancedSettings').click();
// Use the identity switcher to add User Zero
await page.locator('#switch-identity-link').click();
await page.locator('#start-link').click();
// Select "You have a seed" option
await page.getByText('You have a seed').click();
// Get User Zero's seed phrase using the new method
const userZeroData = getTestUserData('00');
// Enter User Zero's seed phrase
await page.getByPlaceholder('Seed Phrase').fill(userZeroData.seedPhrase);
await page.getByRole('button', { name: 'Import' }).click();
// Wait for import to complete
await page.waitForLoadState('networkidle');
// As User Zero, add the auto-created DID as a contact // As User Zero, add the auto-created DID as a contact
await page.goto('./contacts'); await page.goto('./contacts');
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(autoCreatedDid + ', A Friend'); await page.getByPlaceholder('URL or DID, Name, Public Key').fill(autoCreatedDid + ', A Friend');

View File

@@ -1,48 +1,87 @@
import { expect, Page } from '@playwright/test'; import { expect, Page } from "@playwright/test";
// Get test user data based on the ID. // Get test user data based on the ID.
// '01' -> user 111 // '01' -> user 111
// otherwise -> user 000 // otherwise -> user 000
// (... which is a weird convention but I haven't taken the time to change it) // (... which is a weird convention but I haven't taken the time to change it)
export function getTestUserData(id?: string): { seedPhrase: string, userName: string, did: string } { export function getTestUserData(id?: string): {
switch(id) { seedPhrase: string;
case '01': userName: string;
did: string;
} {
switch (id) {
case "01":
return { return {
seedPhrase: 'island fever beef wine urban aim vacant quit afford total poem flame service calm better adult neither color gaze forum month sister imitate excite', seedPhrase:
userName: 'User One', "island fever beef wine urban aim vacant quit afford total poem flame service calm better adult neither color gaze forum month sister imitate excite",
did: 'did:ethr:0x111d15564f824D56C7a07b913aA7aDd03382aA39' userName: "User One",
did: "did:ethr:0x111d15564f824D56C7a07b913aA7aDd03382aA39",
}; };
default: // to user 00 default: // to user 00
return { return {
seedPhrase: 'rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage', seedPhrase:
userName: 'User Zero', "rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage",
did: 'did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F' userName: "User Zero",
did: "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F",
}; };
} }
} }
export async function importUserFromAccount(page: Page, id?: string): Promise<string> {
// Navigate to AccountViewView to use the Identity Switcher
await page.goto("./account");
// Click "Show Advanced Settings" to reveal the identity switcher
await page.getByTestId("advancedSettings").click();
// Use the identity switcher to add User Zero
await page.locator("#switch-identity-link").click();
await page.locator("#start-link").click();
// Select "You have a seed" option
await page.getByText("You have a seed").click();
// Get User Zero's seed phrase using the new method
const userZeroData = getTestUserData(id);
// Enter User Zero's seed phrase
await page.getByPlaceholder("Seed Phrase").fill(userZeroData.seedPhrase);
await page.getByRole("button", { name: "Import" }).click();
// Wait for import to complete
//await page.waitForLoadState("networkidle");
return userZeroData.did;
}
// Import the seed and switch to the user based on the ID. // Import the seed and switch to the user based on the ID.
export async function importUser(page: Page, id?: string): Promise<string> { export async function importUser(page: Page, id?: string): Promise<string> {
const userData = getTestUserData(id); const userData = getTestUserData(id);
const { seedPhrase, userName, did } = userData; const { seedPhrase, userName, did } = userData;
// Import ID // Import ID
await page.goto('./start'); await page.goto("./start");
await page.getByText('You have a seed').click(); await page.getByText("You have a seed").click();
await page.getByPlaceholder('Seed Phrase').fill(seedPhrase); await page.getByPlaceholder("Seed Phrase").fill(seedPhrase);
await page.getByRole('button', { name: 'Import' }).click(); await page.getByRole("button", { name: "Import" }).click();
// Check DID // Check DID
await expect(page.getByRole('code')).toContainText(did); await expect(page.getByRole("code")).toContainText(did);
// ... and ensure the app retrieves the registration status // ... and ensure the app retrieves the registration status
await expect(page.locator('#sectionUsageLimits').getByText('Checking')).toBeHidden(); await expect(
page.locator("#sectionUsageLimits").getByText("Checking")
).toBeHidden();
return did; return did;
} }
export async function importUserAndCloseOnboarding(page: Page, id?: string): Promise<string> { export async function importUserAndCloseOnboarding(
page: Page,
id?: string
): Promise<string> {
const did = await importUser(page, id); const did = await importUser(page, id);
await page.goto('./'); await page.goto("./");
await page.getByTestId('closeOnboardingAndFinish').click(); await page.getByTestId("closeOnboardingAndFinish").click();
return did; return did;
} }
@@ -51,14 +90,29 @@ export async function switchToUser(page: Page, did: string): Promise<void> {
// This is the direct approach but users have to tap on things so we'll do that instead. // This is the direct approach but users have to tap on things so we'll do that instead.
//await page.goto('./identity-switcher'); //await page.goto('./identity-switcher');
await page.goto('./account'); await page.goto("./account");
await page.getByRole('heading', { name: 'Advanced' }).click();
await page.getByRole('link', { name: 'Switch Identifier' }).click(); // Wait for the page to load and the advanced settings element to be visible
await page.waitForLoadState('networkidle');
await page.getByTestId("advancedSettings").waitFor({ state: 'visible' });
const switchIdentityLink = page.locator("#switch-identity-link");
if (await switchIdentityLink.isHidden()) {
console.log("Switch identity link is hidden, clicking advanced settings");
await page.getByTestId("advancedSettings").click();
await switchIdentityLink.click();
} else {
console.log("Switch identity link is visible, clicking it");
await switchIdentityLink.click();
}
const didElem = await page.locator(`code:has-text("${did}")`); const didElem = await page.locator(`code:has-text("${did}")`);
await didElem.isVisible(); await didElem.isVisible();
await didElem.click(); await didElem.click();
// wait for the switch to happen and the account page to fully load // wait for the switch to happen and the account page to fully load
await page.getByTestId('didWrapper').locator('code:has-text("did:")'); await page.getByTestId("didWrapper").locator('code:has-text("did:")');
} }
function createContactName(did: string): string { function createContactName(did: string): string {
@@ -66,24 +120,32 @@ function createContactName(did: string): string {
} }
export async function deleteContact(page: Page, did: string): Promise<void> { export async function deleteContact(page: Page, did: string): Promise<void> {
await page.goto('./contacts'); await page.goto("./contacts");
const contactName = createContactName(did); const contactName = createContactName(did);
// go to the detail page for this contact // go to the detail page for this contact
await page.locator(`li[data-testid="contactListItem"] h2:has-text("${contactName}") + div svg.fa-circle-info`).click(); await page
.locator(
`li[data-testid="contactListItem"] h2:has-text("${contactName}") + div svg.fa-circle-info`
)
.click();
// delete the contact // delete the contact
await page.locator('button > svg.fa-trash-can').click(); await page.locator("button > svg.fa-trash-can").click();
await page.locator('div[role="alert"] button:has-text("Yes")').click(); await page.locator('div[role="alert"] button:has-text("Yes")').click();
// for some reason, .isHidden() (without expect) doesn't work // for some reason, .isHidden() (without expect) doesn't work
await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden(); await expect(
page.locator('div[role="alert"] button:has-text("Yes")')
).toBeHidden();
} }
export async function generateNewEthrUser(page: Page): Promise<string> { export async function generateNewEthrUser(page: Page): Promise<string> {
await page.goto('./start'); await page.goto("./start");
await page.getByTestId('newSeed').click(); await page.getByTestId("newSeed").click();
await expect(page.locator('span:has-text("Created")')).toBeVisible(); await expect(page.locator('span:has-text("Created")')).toBeVisible();
await page.goto('./account'); await page.goto("./account");
const didElem = await page.getByTestId('didWrapper').locator('code:has-text("did:")'); const didElem = await page
.getByTestId("didWrapper")
.locator('code:has-text("did:")');
const newDid = await didElem.innerText(); const newDid = await didElem.innerText();
return newDid; return newDid;
} }
@@ -93,28 +155,36 @@ export async function generateNewEthrUser(page: Page): Promise<string> {
export async function generateAndRegisterEthrUser(page: Page): Promise<string> { export async function generateAndRegisterEthrUser(page: Page): Promise<string> {
const newDid = await generateNewEthrUser(page); const newDid = await generateNewEthrUser(page);
await importUser(page, '000'); // switch to user 000 await importUser(page, "000"); // switch to user 000
await page.goto('./contacts'); await page.goto("./contacts");
const contactName = createContactName(newDid); const contactName = createContactName(newDid);
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(`${newDid}, ${contactName}`); await page
await page.locator('button > svg.fa-plus').click(); .getByPlaceholder("URL or DID, Name, Public Key")
.fill(`${newDid}, ${contactName}`);
await page.locator("button > svg.fa-plus").click();
// register them // register them
await page.locator('div[role="alert"] button:has-text("Yes")').click(); await page.locator('div[role="alert"] button:has-text("Yes")').click();
// wait for it to disappear because the next steps may depend on alerts being gone // wait for it to disappear because the next steps may depend on alerts being gone
await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden(); await expect(
await expect(page.locator('li', { hasText: contactName })).toBeVisible(); page.locator('div[role="alert"] button:has-text("Yes")')
).toBeHidden();
await expect(page.locator("li", { hasText: contactName })).toBeVisible();
return newDid; return newDid;
} }
// Function to generate a random string of specified length // Function to generate a random string of specified length
export async function generateRandomString(length: number): Promise<string> { export async function generateRandomString(length: number): Promise<string> {
return Math.random().toString(36).substring(2, 2 + length); return Math.random()
.toString(36)
.substring(2, 2 + length);
} }
// Function to create an array of unique strings // Function to create an array of unique strings
export async function createUniqueStringsArray(count: number): Promise<string[]> { export async function createUniqueStringsArray(
count: number
): Promise<string[]> {
const stringsArray: string[] = []; const stringsArray: string[] = [];
const stringLength = 16; const stringLength = 16;
@@ -127,7 +197,9 @@ export async function createUniqueStringsArray(count: number): Promise<string[]>
} }
// Function to create an array of two-digit non-zero numbers // Function to create an array of two-digit non-zero numbers
export async function createRandomNumbersArray(count: number): Promise<number[]> { export async function createRandomNumbersArray(
count: number
): Promise<number[]> {
const numbersArray: number[] = []; const numbersArray: number[] = [];
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
@@ -139,35 +211,37 @@ export async function createRandomNumbersArray(count: number): Promise<number[]>
} }
export function isLinuxEnvironment() { export function isLinuxEnvironment() {
return process.platform === 'linux'; return process.platform === "linux";
} }
export function getOSSpecificTimeout(): number { export function getOSSpecificTimeout(): number {
// Increase base timeout for Linux // Increase base timeout for Linux
const isLinux = process.platform === 'linux'; const isLinux = process.platform === "linux";
return isLinux ? 180000 : 60000; // 3 minutes for Linux, 1 minute for others return isLinux ? 180000 : 60000; // 3 minutes for Linux, 1 minute for others
} }
export function getOSSpecificConfig() { export function getOSSpecificConfig() {
if (isLinuxEnvironment()) { if (isLinuxEnvironment()) {
return { return {
retries: 2, retries: 2,
timeout: 90000, // Increased global timeout timeout: 90000, // Increased global timeout
expect: { expect: {
timeout: 30000 // Increased expect timeout timeout: 30000, // Increased expect timeout
}, },
// Add video recording for failed tests on Linux // Add video recording for failed tests on Linux
use: { use: {
video: 'retain-on-failure', video: "retain-on-failure",
trace: 'retain-on-failure' trace: "retain-on-failure",
} },
}; };
} }
return {}; return {};
} }
// Add helper for test grouping // Add helper for test grouping
export function isResourceIntensiveTest(testPath: string): boolean { export function isResourceIntensiveTest(testPath: string): boolean {
return testPath.includes('35-record-gift-from-image-share') || return (
testPath.includes('40-add-contact'); testPath.includes("35-record-gift-from-image-share") ||
testPath.includes("40-add-contact")
);
} }