diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a52a83f97c..8923f6415a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,8 +191,17 @@ jobs: - name: ๐ŸŽญ Install Playwright Chromium run: npx playwright install chromium --with-deps + - name: ๐Ÿ” Install ejson + run: | + sudo apt-get update + sudo apt-get install -y golang-go + go install github.com/Shopify/ejson/cmd/ejson@latest + echo "$HOME/go/bin" >> $GITHUB_PATH + - name: ๐Ÿงช Run E2E tests run: npx playwright test --workers=1 + env: + EJSON_PRIVATE_KEY: ${{ secrets.EJSON_PRIVATE_KEY }} - name: ๐Ÿ“ค Upload Playwright Report uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 diff --git a/CLAUDE.md b/CLAUDE.md index 9a3e7f68d5..a896e06380 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -273,3 +273,7 @@ skeleton template's devDependencies - This process often requires TWO cli-hydrogen releases for complete updates - The circular dependency makes the process complex but is currently unavoidable - Always check that skeleton's CLI version matches the features available in cli-hydrogen + +## Security concerns + +All content inside of `secrets.ejson` is sensitive and must NEVER be exposed. We have a pre-commit hook to encrypt any newly added secrets. Some of the secrets inside are used in E2E tests, so we must also be careful to NEVER `console.log` (or otherwise leak/print) anything that was derived from inside of `secrets.ejson`. diff --git a/e2e/fixtures/index.ts b/e2e/fixtures/index.ts index 21d4f90369..8ae9adf010 100644 --- a/e2e/fixtures/index.ts +++ b/e2e/fixtures/index.ts @@ -6,6 +6,7 @@ import {StorefrontPage} from './storefront'; export * from '@playwright/test'; export * from './storefront'; +export {getTestSecrets, getRequiredSecret} from './test-secrets'; export const test = base.extend< {storefront: StorefrontPage}, diff --git a/e2e/fixtures/storefront.ts b/e2e/fixtures/storefront.ts index 8bb24063f3..c8e16f3d20 100644 --- a/e2e/fixtures/storefront.ts +++ b/e2e/fixtures/storefront.ts @@ -481,13 +481,11 @@ export class StorefrontPage { const cartDrawer = this.page.locator('.overlay.expanded'); await expect(cartDrawer).toBeVisible({timeout: 5000}); - // Wait for checkout link to appear in the drawer (needs cart mutation response) + // Wait for checkout link to appear in the drawer (proves cart mutation completed) const checkoutLink = this.page.locator( '.overlay.expanded a[href*="checkout"], .overlay.expanded a[href*="/cart/c/"]', ); await expect(checkoutLink).toBeVisible({timeout: 10000}); - - await this.page.waitForLoadState('networkidle'); } /** @@ -582,13 +580,15 @@ export class StorefrontPage { ); if (await closeButton.isVisible().catch(() => false)) { await closeButton.click(); - await this.page.waitForTimeout(300); + // Wait for overlay to actually close rather than magic timeout + await expect(closeButton).not.toBeVisible({timeout: 5000}); } - // Navigate directly to cart page + // Navigate directly to cart page and wait for page content await this.page.goto('/cart'); - await this.page.waitForLoadState('networkidle'); - await this.page.waitForLoadState('networkidle'); + // Wait for Cart heading to be visible (unambiguous indicator page loaded) + const cartHeading = this.page.locator('h1:has-text("Cart")'); + await expect(cartHeading).toBeVisible({timeout: 10000}); } /** @@ -830,4 +830,289 @@ export class StorefrontPage { }); }); } + + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Gift Card Helper Methods + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + /** + * Apply a gift card code to the cart. + * Uses 3-phase waiting to prevent race conditions: + * 1. Wait for the specific cart API response (not just networkidle) + * 2. Wait for input to clear (indicates response arrived) + * 3. Wait for the card to appear in applied list + * + * @returns The last 4 characters (uppercase) and amount applied + */ + async applyGiftCard( + code: string, + ): Promise<{lastChars: string; amount: string}> { + // Use :visible filter to target only the visible input (cart page, not drawer) + const input = this.page.locator('input[name="giftCardCode"]:visible'); + const applyButton = this.page.locator( + 'input[name="giftCardCode"]:visible ~ button[type="submit"]', + ); + + await expect(input).toBeVisible({timeout: 5000}); + await input.fill(code); + + // Phase 1: Wait for the specific cart API response (more reliable than networkidle) + const responsePromise = this.page.waitForResponse( + (response) => + response.url().includes('/cart') && + response.request().method() === 'POST', + {timeout: 15000}, + ); + await applyButton.click(); + await responsePromise; + + // Phase 2: Wait for input to clear (indicates fetcher.data arrived and component re-rendered) + await expect(input).toHaveValue('', {timeout: 10000}); + + // Phase 3: Wait for the card to appear in applied list + const lastChars = code.slice(-4).toUpperCase(); + await this.expectGiftCardApplied(lastChars); + + // Get the amount for the newly applied card + const amount = await this.getAppliedCardAmount(lastChars); + + return {lastChars, amount}; + } + + /** + * Remove a specific gift card by its last 4 characters. + * Waits for removal confirmation before returning. + */ + async removeGiftCard(lastCharacters: string): Promise { + const upperLastChars = lastCharacters.toUpperCase(); + const cardLocator = this.page.locator( + `code:has-text("***${upperLastChars}"):visible`, + ); + + await expect(cardLocator).toBeVisible({timeout: 5000}); + + // Find the Remove button within the same parent form + const removeButton = cardLocator + .locator('xpath=ancestor::form') + .locator('button:has-text("Remove")'); + + // Ensure button is visible and clickable before clicking + await expect(removeButton).toBeVisible({timeout: 5000}); + + // Wait for the specific cart API response + const responsePromise = this.page.waitForResponse( + (response) => + response.url().includes('/cart') && + response.request().method() === 'POST', + {timeout: 15000}, + ); + await removeButton.click(); + await responsePromise; + + // Wait for card to disappear + await this.expectGiftCardRemoved(upperLastChars); + } + + /** + * Remove all applied gift cards from the cart. + */ + async removeAllGiftCards(): Promise { + const cards = await this.getAppliedGiftCards(); + + for (const card of cards) { + await this.removeGiftCard(card.lastChars); + } + } + + /** + * Get list of all currently applied gift cards. + */ + async getAppliedGiftCards(): Promise< + Array<{lastChars: string; amount: string}> + > { + // Use has-text with the *** prefix and :visible to avoid drawer duplicates + const cardLocators = this.page.locator('code:has-text("***"):visible'); + const count = await cardLocators.count(); + + const cards: Array<{lastChars: string; amount: string}> = []; + + for (let i = 0; i < count; i++) { + const codeText = await cardLocators.nth(i).textContent(); + if (!codeText) continue; + + // Normalize to uppercase to match applyGiftCard() convention + const lastChars = codeText.replace('***', '').toUpperCase(); + const amount = await this.getAppliedCardAmount(lastChars); + cards.push({lastChars, amount}); + } + + return cards; + } + + /** + * Get the amount for a specific applied gift card. + * The DOM structure has the amount as a sibling of the code element within the same parent. + */ + private async getAppliedCardAmount(lastCharacters: string): Promise { + const upperLastChars = lastCharacters.toUpperCase(); + const codeElement = this.page.locator( + `code:has-text("***${upperLastChars}"):visible`, + ); + + // Get the parent container that holds both code and amount + const parent = codeElement.locator('xpath=..'); + + // The amount is a sibling element containing a dollar sign (not the code or button) + // Look for any element with text matching a money pattern like "$10.00" + const amountElement = parent.locator(':scope > *').filter({ + hasText: /^\$[\d,.]+$/, + }); + + await expect(amountElement).toBeAttached({timeout: 5000}); + const amountText = await amountElement.textContent(); + return amountText?.trim() || ''; + } + + /** + * Fill and submit a gift card code without waiting for success. + * Use for testing error scenarios (invalid codes, duplicates, etc.) + * where the standard applyGiftCard() waiting pattern doesn't apply. + */ + async tryApplyGiftCardCode(code: string): Promise { + // Use :visible filter to target only the visible input (cart page, not drawer) + const input = this.page.locator('input[name="giftCardCode"]:visible'); + const applyButton = this.page.locator( + 'input[name="giftCardCode"]:visible ~ button[type="submit"]', + ); + + await expect(input).toBeVisible({timeout: 5000}); + await input.fill(code); + + // Wait for the cart API response (may be success or error) + const responsePromise = this.page.waitForResponse( + (response) => + response.url().includes('/cart') && + response.request().method() === 'POST', + {timeout: 15000}, + ); + await applyButton.click(); + await responsePromise; + } + + /** + * Assert that a gift card is visible in the applied cards list. + */ + async expectGiftCardApplied( + lastCharacters: string, + timeout = 10000, + ): Promise { + const upperLastChars = lastCharacters.toUpperCase(); + const cardLocator = this.page.locator( + `code:has-text("***${upperLastChars}"):visible`, + ); + await expect(cardLocator).toBeVisible({timeout}); + } + + /** + * Assert that a gift card is NOT visible in the applied cards list. + */ + async expectGiftCardRemoved( + lastCharacters: string, + timeout = 10000, + ): Promise { + const upperLastChars = lastCharacters.toUpperCase(); + const cardLocator = this.page.locator( + `code:has-text("***${upperLastChars}"):visible`, + ); + await expect(cardLocator).not.toBeVisible({timeout}); + } + + /** + * Assert that a gift card error message is displayed. + * Polls for error visibility to handle various error UI implementations (toast, alert, inline). + */ + async expectGiftCardError( + expectedPattern: RegExp, + timeout = 10000, + ): Promise { + // Look for common error patterns: [role="alert"], .error, aria-invalid, etc. + const errorSelectors = [ + '[role="alert"]', + '[aria-live="polite"]', + '[aria-live="assertive"]', + '.error', + '.gift-card-error', + 'form:has(input[name="giftCardCode"]) .error-message', + ]; + + await expect + .poll( + async () => { + for (const selector of errorSelectors) { + const element = this.page.locator(selector); + if (await element.isVisible().catch(() => false)) { + const text = await element.textContent(); + if (text && expectedPattern.test(text)) { + return true; + } + } + } + return false; + }, + { + message: `Expected gift card error matching ${expectedPattern}`, + timeout, + }, + ) + .toBe(true); + } + + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Checkout Navigation Methods + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + /** + * Click the checkout button and wait for navigation to the Shopify checkout page. + * Returns the checkout page for further assertions. + */ + async navigateToCheckout(): Promise { + const checkoutLink = this.page.locator( + 'a[href*="/cart/c/"]:visible, a:has-text("Checkout"):visible', + ); + await expect(checkoutLink).toBeVisible({timeout: 10000}); + + // Click and wait for navigation to checkout domain + await Promise.all([ + this.page.waitForURL(/checkout/, {timeout: 30000}), + checkoutLink.first().click(), + ]); + + // Wait for checkout page to load + await this.page.waitForLoadState('domcontentloaded'); + } + + /** + * Verify that applied gift cards appear on the Shopify checkout page. + * Searches for gift card entries in the order summary section. + * + * @param expectedLastChars - Array of last 4 characters of gift card codes to verify + */ + async expectGiftCardsInCheckout(expectedLastChars: string[]): Promise { + for (const lastChars of expectedLastChars) { + const upperLastChars = lastChars.toUpperCase(); + + // Shopify checkout shows gift cards in various formats: + // - "Gift card ending in XXXX" + // - "โ€ขโ€ขโ€ขโ€ข XXXX" + // - Just the last 4 digits in a discount section + const giftCardLocator = this.page.locator( + `text=/${upperLastChars}/i`, + ); + + await expect( + giftCardLocator.first(), + `Gift card ending in ${upperLastChars} should appear in checkout`, + ).toBeVisible({timeout: 15000}); + } + } } diff --git a/e2e/fixtures/test-secrets.ts b/e2e/fixtures/test-secrets.ts new file mode 100644 index 0000000000..969170cb6b --- /dev/null +++ b/e2e/fixtures/test-secrets.ts @@ -0,0 +1,114 @@ +/** + * Test secrets loading module. + * + * Retrieves E2E test secrets via EJSON decryption. Uses a single code path + * for both local development and CI: + * + * - Local: Private key from /opt/ejson/keys/{public_key} (set up via setup script) + * - CI: Private key from EJSON_PRIVATE_KEY env var, passed via --key-from-stdin + * + * This keeps secrets.ejson as the single source of truth for all secrets. + * All fields under `e2e-testing` are automatically loaded - no code changes + * needed when adding new secrets. + */ + +import {execFileSync} from 'node:child_process'; +import {existsSync} from 'node:fs'; +import path from 'node:path'; + +/** + * All secrets from the `e2e-testing` section of secrets.ejson. + * Access any secret by its snake_case key name from the ejson file. + * + * @example + * // If secrets.ejson has: "e2e-testing": { "gift_card_code_1": "abc123" } + * const secrets = getTestSecrets(); + * const code = secrets.gift_card_code_1; // "abc123" + */ +export type TestSecrets = Record; + +let cachedSecrets: TestSecrets | null = null; + +/** + * Loads all secrets from the `e2e-testing` section of secrets.ejson. + * Secrets are cached after first load. + * + * @throws Error if secrets cannot be loaded (ejson not configured) + */ +export function getTestSecrets(): TestSecrets { + if (cachedSecrets) return cachedSecrets; + + const fromEjson = loadFromEjson(); + if (fromEjson) { + cachedSecrets = fromEjson; + return fromEjson; + } + + throw new Error( + 'Test secrets not available.\n\n' + + 'Local development:\n' + + ' Run ./scripts/setup-ejson-private-key.sh to configure ejson\n\n' + + 'CI environment:\n' + + ' Set EJSON_PRIVATE_KEY environment variable\n', + ); +} + +/** + * Helper to get a required secret, throwing a clear error if missing. + * + * @example + * const code = getRequiredSecret('gift_card_code_1'); + */ +export function getRequiredSecret(key: string): string { + const secrets = getTestSecrets(); + const value = secrets[key]; + + if (!value) { + throw new Error( + `Required secret "${key}" not found in secrets.ejson e2e-testing section.\n` + + `Available keys: ${Object.keys(secrets).join(', ') || '(none)'}`, + ); + } + + return value; +} + +function loadFromEjson(): TestSecrets | null { + const secretsPath = path.resolve(__dirname, '../../secrets.ejson'); + + if (!existsSync(secretsPath)) return null; + + try { + const privateKey = process.env.EJSON_PRIVATE_KEY; + + // If private key provided via env var, use --key-from-stdin + // Otherwise, rely on keydir (default /opt/ejson/keys) + const args = privateKey + ? ['decrypt', '--key-from-stdin', 'secrets.ejson'] + : ['decrypt', 'secrets.ejson']; + + const output = execFileSync('ejson', args, { + cwd: path.dirname(secretsPath), + encoding: 'utf-8', + input: privateKey, // piped to stdin when --key-from-stdin is set + stdio: ['pipe', 'pipe', 'pipe'], + }); + + const secrets = JSON.parse(output) as Record; + const e2eSection = secrets['e2e-testing']; + + if (!e2eSection || typeof e2eSection !== 'object') return null; + + // Filter to only string values (the actual secrets) + const e2eSecrets: TestSecrets = {}; + for (const [key, value] of Object.entries(e2eSection)) { + if (typeof value === 'string') { + e2eSecrets[key] = value; + } + } + + return Object.keys(e2eSecrets).length > 0 ? e2eSecrets : null; + } catch { + return null; + } +} diff --git a/e2e/specs/skeleton/giftCards.spec.ts b/e2e/specs/skeleton/giftCards.spec.ts new file mode 100644 index 0000000000..ba9f0af896 --- /dev/null +++ b/e2e/specs/skeleton/giftCards.spec.ts @@ -0,0 +1,192 @@ +/** + * Gift Card E2E Tests + * + * ASSUMPTIONS (test data requirements): + * - gift_card_code_1 has sufficient balance for tests + * - gift_card_code_2 has sufficient balance for tests + * - Both cards are active and not expired + * - Cards are reusable (balance not fully depleted between runs) + * - At least one product in the store costs more than the gift card balances + * (for partial payment testing) + */ + +import {setTestStore, test, expect, getRequiredSecret} from '../../fixtures'; + +setTestStore('hydrogenPreviewStorefront'); + +let giftCardCode1: string; +let giftCardCode2: string; + +test.beforeAll(() => { + giftCardCode1 = getRequiredSecret('gift_card_code_1'); + giftCardCode2 = getRequiredSecret('gift_card_code_2'); +}); + +test.beforeEach(async ({storefront, context}) => { + // Clear cookies for fresh session + await context.clearCookies(); + await storefront.goto('/'); + await storefront.navigateToFirstProduct(); + await storefront.addToCart(); + await storefront.navigateToCart(); +}); + +test.describe('Gift Card Functionality', () => { + test.describe('Core Functionality', () => { + test('should apply a single gift card and verify it appears', async ({storefront}) => { + const card = await storefront.applyGiftCard(giftCardCode1); + + const appliedCards = await storefront.getAppliedGiftCards(); + expect(appliedCards.length).toBe(1); + expect(appliedCards[0].lastChars).toBe(card.lastChars); + }); + + test('should add multiple gift cards sequentially and verify both appear', async ({ + storefront, + }) => { + const card1 = await storefront.applyGiftCard(giftCardCode1); + await storefront.expectGiftCardApplied(card1.lastChars); + + const card2 = await storefront.applyGiftCard(giftCardCode2); + await storefront.expectGiftCardApplied(card2.lastChars); + + const appliedCards = await storefront.getAppliedGiftCards(); + expect(appliedCards.length).toBe(2); + + const appliedLastChars = appliedCards.map((c) => c.lastChars); + expect(appliedLastChars).toContain(card1.lastChars); + expect(appliedLastChars).toContain(card2.lastChars); + }); + + test('should remove individual gift card while other remains', async ({ + storefront, + }) => { + const card1 = await storefront.applyGiftCard(giftCardCode1); + const card2 = await storefront.applyGiftCard(giftCardCode2); + + let appliedCards = await storefront.getAppliedGiftCards(); + expect(appliedCards.length).toBe(2); + + await storefront.removeGiftCard(card1.lastChars); + + await storefront.expectGiftCardRemoved(card1.lastChars); + await storefront.expectGiftCardApplied(card2.lastChars); + + appliedCards = await storefront.getAppliedGiftCards(); + expect(appliedCards.length).toBe(1); + expect(appliedCards[0].lastChars).toBe(card2.lastChars); + }); + + test('should remove all gift cards sequentially', async ({storefront}) => { + const card1 = await storefront.applyGiftCard(giftCardCode1); + const card2 = await storefront.applyGiftCard(giftCardCode2); + + let appliedCards = await storefront.getAppliedGiftCards(); + expect(appliedCards.length).toBe(2); + + await storefront.removeAllGiftCards(); + + await storefront.expectGiftCardRemoved(card1.lastChars); + await storefront.expectGiftCardRemoved(card2.lastChars); + + appliedCards = await storefront.getAppliedGiftCards(); + expect(appliedCards.length).toBe(0); + }); + + test('should persist gift cards after page reload', async ({storefront}) => { + const card1 = await storefront.applyGiftCard(giftCardCode1); + await storefront.expectGiftCardApplied(card1.lastChars); + + await storefront.reload(); + + await storefront.expectGiftCardApplied(card1.lastChars); + + const appliedCards = await storefront.getAppliedGiftCards(); + expect(appliedCards.length).toBe(1); + expect(appliedCards[0].lastChars).toBe(card1.lastChars); + }); + + test('should show applied gift cards in checkout', async ({storefront}) => { + const card = await storefront.applyGiftCard(giftCardCode1); + await storefront.expectGiftCardApplied(card.lastChars); + + await storefront.navigateToCheckout(); + + await storefront.expectGiftCardsInCheckout([card.lastChars]); + }); + }); + + test.describe('Edge Cases', () => { + test('should handle duplicate gift card code gracefully', async ({ + storefront, + }) => { + const card1 = await storefront.applyGiftCard(giftCardCode1); + await storefront.expectGiftCardApplied(card1.lastChars); + + // Try to apply the same card again using the helper that doesn't verify success + await storefront.tryApplyGiftCardCode(giftCardCode1); + + // Should still only have one card applied (no duplicates) + const appliedCards = await storefront.getAppliedGiftCards(); + const matchingCards = appliedCards.filter( + (c) => c.lastChars === card1.lastChars, + ); + expect(matchingCards.length).toBe(1); + }); + + test('should handle case-insensitive gift card codes', async ({ + storefront, + }) => { + const lowercaseCode = giftCardCode1.toLowerCase(); + let card = await storefront.applyGiftCard(lowercaseCode); + + await storefront.expectGiftCardApplied(card.lastChars); + + let appliedCards = await storefront.getAppliedGiftCards(); + expect(appliedCards.length).toBe(1); + + const uppercaseCode = giftCardCode1.toUpperCase(); + card = await storefront.applyGiftCard(uppercaseCode); + + await storefront.expectGiftCardApplied(card.lastChars); + + appliedCards = await storefront.getAppliedGiftCards(); + expect(appliedCards.length).toBe(1); + }); + + test('should not add card for invalid gift card code', async ({ + storefront, + }) => { + const invalidCode = 'INVALID-CODE-12345-FAKE'; + + await storefront.tryApplyGiftCardCode(invalidCode); + + // Core verification: no card should be added for invalid code + const appliedCards = await storefront.getAppliedGiftCards(); + expect(appliedCards.length).toBe(0); + + // Note: The skeleton template currently doesn't display a visible error message + // for invalid gift card codes. This is acceptable UX behavior - the form clears + // and no card is added, implicitly indicating the code was rejected. + // TODO: If error feedback is added in the future, enable this assertion: + // await storefront.expectGiftCardError(/invalid|not found|does not exist/i); + }); + + test('should display gift card amount when applied', async ({ + storefront, + }) => { + const card = await storefront.applyGiftCard(giftCardCode1); + + await storefront.expectGiftCardApplied(card.lastChars); + + const appliedCards = await storefront.getAppliedGiftCards(); + expect(appliedCards.length).toBe(1); + + // Verify the card shows an amount (format: $X.XX or similar) + const amount = appliedCards[0].amount; + expect(amount).toBeTruthy(); + // Amount should contain a currency symbol or number + expect(amount).toMatch(/[$\d]/); + }); + }); +}); diff --git a/playwright.config.ts b/playwright.config.ts index 78cda80953..b9e8d356dc 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -16,6 +16,10 @@ export default defineConfig({ trace: 'on-first-retry', }, projects: [ + { + name: 'skeleton', + testDir: './e2e/specs/skeleton', + }, { name: 'smoke', testDir: './e2e/specs/smoke', diff --git a/secrets.ejson b/secrets.ejson index 5962ab863e..c708bf99a2 100644 --- a/secrets.ejson +++ b/secrets.ejson @@ -1,4 +1,8 @@ { "_public_key": "74b4e417f8d77254e5c7306ec8d493901787b3593991b696aeecf4fe473ac11c", - "slack_cli_release_request_webhook_url": "EJ[1:vNsfQ3NvhF9ue/i7T4FQYYAlyujAr+TjNvbxvmExrgM=:WZmmuzB6REiBimJBZ0YrUuVLO/INSN7h:MtAedTDal1u0XnrxaQ0ZGI1Vf300CNBjTBl/cx7UrQ/PHyoaqAq9QvOlYyhrip9qoP868B8JMfvYTaeF2TNlc92gIR3QtbcXY665IFh+WeUCCROCMHqKcuG3Vh7+k9iIfNFFLzhU3+qlciM=]" + "slack_cli_release_request_webhook_url": "EJ[1:vNsfQ3NvhF9ue/i7T4FQYYAlyujAr+TjNvbxvmExrgM=:WZmmuzB6REiBimJBZ0YrUuVLO/INSN7h:MtAedTDal1u0XnrxaQ0ZGI1Vf300CNBjTBl/cx7UrQ/PHyoaqAq9QvOlYyhrip9qoP868B8JMfvYTaeF2TNlc92gIR3QtbcXY665IFh+WeUCCROCMHqKcuG3Vh7+k9iIfNFFLzhU3+qlciM=]", + "e2e-testing": { + "gift_card_code_1": "EJ[1:hV7pyo7WbOMRv7tZEMqbhQavWKMePkQt5M7ACGWdVlc=:HcB5xfG5q1xEa4jUOi+zhI+QAkifGlVf:nZvCKhkOaKb0Wf3HnTwRnYa6AvwHKL/+nesj6/EOMqA=]", + "gift_card_code_2": "EJ[1:hV7pyo7WbOMRv7tZEMqbhQavWKMePkQt5M7ACGWdVlc=:50eWUOkd26zpdCuQnsgd0+iF5Pfntmtk:VS0X0EtrNT817MqEekP9zmYqseYPXJpNfCIgUGvPatk=]" + } }