Browse Source

WIP: improve font caching and partial fix on test

pull/142/head
Matthew Raymer 9 hours ago
parent
commit
e04c9f3626
  1. 4
      electron/src/setup.ts
  2. 117
      scripts/download-fonts.sh
  3. BIN
      src/assets/fonts/WorkSans-Bold.ttf
  4. BIN
      src/assets/fonts/WorkSans-BoldItalic.ttf
  5. BIN
      src/assets/fonts/WorkSans-Italic.ttf
  6. BIN
      src/assets/fonts/WorkSans-Light.ttf
  7. BIN
      src/assets/fonts/WorkSans-LightItalic.ttf
  8. BIN
      src/assets/fonts/WorkSans-Medium.ttf
  9. BIN
      src/assets/fonts/WorkSans-MediumItalic.ttf
  10. BIN
      src/assets/fonts/WorkSans-Regular.ttf
  11. BIN
      src/assets/fonts/WorkSans-SemiBold.ttf
  12. BIN
      src/assets/fonts/WorkSans-SemiBoldItalic.ttf
  13. 81
      src/assets/styles/fonts.css
  14. 2
      src/assets/styles/tailwind.css
  15. 29
      test-playwright/60-new-activity.spec.ts
  16. 192
      test-playwright/testUtils.ts

4
electron/src/setup.ts

@ -225,8 +225,8 @@ export function setupContentSecurityPolicy(customScheme: string): void {
...details.responseHeaders,
'Content-Security-Policy': [
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' data: https:; 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'; font-src ${customScheme}://* data:`,
],
},
});

117
scripts/download-fonts.sh

@ -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!"

BIN
src/assets/fonts/WorkSans-Bold.ttf

Binary file not shown.

BIN
src/assets/fonts/WorkSans-BoldItalic.ttf

Binary file not shown.

BIN
src/assets/fonts/WorkSans-Italic.ttf

Binary file not shown.

BIN
src/assets/fonts/WorkSans-Light.ttf

Binary file not shown.

BIN
src/assets/fonts/WorkSans-LightItalic.ttf

Binary file not shown.

BIN
src/assets/fonts/WorkSans-Medium.ttf

Binary file not shown.

BIN
src/assets/fonts/WorkSans-MediumItalic.ttf

Binary file not shown.

BIN
src/assets/fonts/WorkSans-Regular.ttf

Binary file not shown.

BIN
src/assets/fonts/WorkSans-SemiBold.ttf

Binary file not shown.

BIN
src/assets/fonts/WorkSans-SemiBoldItalic.ttf

Binary file not shown.

81
src/assets/styles/fonts.css

@ -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');
}

2
src/assets/styles/tailwind.css

@ -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 components;
@tailwind utilities;

29
test-playwright/60-new-activity.spec.ts

@ -1,5 +1,5 @@
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 }) => {
await page.goto('./');
@ -15,30 +15,9 @@ test('New offers for another user', async ({ page }) => {
await page.getByTestId('closeOnboardingAndFinish').click();
await expect(page.getByTestId('newDirectOffersActivityNumber')).toBeHidden();
// 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('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');
// Become User Zero
await importUserFromAccount(page, "00");
// As User Zero, add the auto-created DID as a contact
await page.goto('./contacts');
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(autoCreatedDid + ', A Friend');

192
test-playwright/testUtils.ts

@ -1,48 +1,87 @@
import { expect, Page } from '@playwright/test';
import { expect, Page } from "@playwright/test";
// Get test user data based on the ID.
// '01' -> user 111
// otherwise -> user 000
// (... 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 } {
switch(id) {
case '01':
export function getTestUserData(id?: string): {
seedPhrase: string;
userName: string;
did: string;
} {
switch (id) {
case "01":
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',
userName: 'User One',
did: 'did:ethr:0x111d15564f824D56C7a07b913aA7aDd03382aA39'
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",
userName: "User One",
did: "did:ethr:0x111d15564f824D56C7a07b913aA7aDd03382aA39",
};
default: // to user 00
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',
userName: 'User Zero',
did: 'did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F'
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",
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.
export async function importUser(page: Page, id?: string): Promise<string> {
const userData = getTestUserData(id);
const { seedPhrase, userName, did } = userData;
// Import ID
await page.goto('./start');
await page.getByText('You have a seed').click();
await page.getByPlaceholder('Seed Phrase').fill(seedPhrase);
await page.getByRole('button', { name: 'Import' }).click();
await page.goto("./start");
await page.getByText("You have a seed").click();
await page.getByPlaceholder("Seed Phrase").fill(seedPhrase);
await page.getByRole("button", { name: "Import" }).click();
// Check DID
await expect(page.getByRole('code')).toContainText(did);
await expect(page.getByRole("code")).toContainText(did);
// ... 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;
}
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);
await page.goto('./');
await page.getByTestId('closeOnboardingAndFinish').click();
await page.goto("./");
await page.getByTestId("closeOnboardingAndFinish").click();
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.
//await page.goto('./identity-switcher');
await page.goto('./account');
await page.getByRole('heading', { name: 'Advanced' }).click();
await page.getByRole('link', { name: 'Switch Identifier' }).click();
await page.goto("./account");
// 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}")`);
await didElem.isVisible();
await didElem.click();
// 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 {
@ -66,24 +120,32 @@ function createContactName(did: string): string {
}
export async function deleteContact(page: Page, did: string): Promise<void> {
await page.goto('./contacts');
await page.goto("./contacts");
const contactName = createContactName(did);
// 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
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();
// 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> {
await page.goto('./start');
await page.getByTestId('newSeed').click();
await page.goto("./start");
await page.getByTestId("newSeed").click();
await expect(page.locator('span:has-text("Created")')).toBeVisible();
await page.goto('./account');
const didElem = await page.getByTestId('didWrapper').locator('code:has-text("did:")');
await page.goto("./account");
const didElem = await page
.getByTestId("didWrapper")
.locator('code:has-text("did:")');
const newDid = await didElem.innerText();
return newDid;
}
@ -93,28 +155,36 @@ export async function generateNewEthrUser(page: Page): Promise<string> {
export async function generateAndRegisterEthrUser(page: Page): Promise<string> {
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);
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(`${newDid}, ${contactName}`);
await page.locator('button > svg.fa-plus').click();
await page
.getByPlaceholder("URL or DID, Name, Public Key")
.fill(`${newDid}, ${contactName}`);
await page.locator("button > svg.fa-plus").click();
// register them
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
await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden();
await expect(page.locator('li', { hasText: contactName })).toBeVisible();
await expect(
page.locator('div[role="alert"] button:has-text("Yes")')
).toBeHidden();
await expect(page.locator("li", { hasText: contactName })).toBeVisible();
return newDid;
}
// Function to generate a random string of specified length
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
export async function createUniqueStringsArray(count: number): Promise<string[]> {
export async function createUniqueStringsArray(
count: number
): Promise<string[]> {
const stringsArray: string[] = [];
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
export async function createRandomNumbersArray(count: number): Promise<number[]> {
export async function createRandomNumbersArray(
count: number
): Promise<number[]> {
const numbersArray: number[] = [];
for (let i = 0; i < count; i++) {
@ -139,35 +211,37 @@ export async function createRandomNumbersArray(count: number): Promise<number[]>
}
export function isLinuxEnvironment() {
return process.platform === 'linux';
return process.platform === "linux";
}
export function getOSSpecificTimeout(): number {
// 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
}
export function getOSSpecificConfig() {
if (isLinuxEnvironment()) {
return {
retries: 2,
timeout: 90000, // Increased global timeout
expect: {
timeout: 30000 // Increased expect timeout
},
// Add video recording for failed tests on Linux
use: {
video: 'retain-on-failure',
trace: 'retain-on-failure'
}
};
}
return {};
if (isLinuxEnvironment()) {
return {
retries: 2,
timeout: 90000, // Increased global timeout
expect: {
timeout: 30000, // Increased expect timeout
},
// Add video recording for failed tests on Linux
use: {
video: "retain-on-failure",
trace: "retain-on-failure",
},
};
}
return {};
}
// Add helper for test grouping
export function isResourceIntensiveTest(testPath: string): boolean {
return testPath.includes('35-record-gift-from-image-share') ||
testPath.includes('40-add-contact');
return (
testPath.includes("35-record-gift-from-image-share") ||
testPath.includes("40-add-contact")
);
}

Loading…
Cancel
Save