diff --git a/.github/workflows/test-starter-template.yml b/.github/workflows/test-starter-template.yml new file mode 100644 index 0000000..963515c --- /dev/null +++ b/.github/workflows/test-starter-template.yml @@ -0,0 +1,19 @@ +name: Test Starter Template + +on: + push: + branches: [main] + paths: + - 'templates/starter/**' + - 'tests/**' + pull_request: + branches: [main] + paths: + - 'templates/starter/**' + - 'tests/**' + +jobs: + test-starter-template: + uses: ./.github/workflows/test-template.yml + with: + template: starter diff --git a/.github/workflows/test-template.yml b/.github/workflows/test-template.yml new file mode 100644 index 0000000..bb2a21c --- /dev/null +++ b/.github/workflows/test-template.yml @@ -0,0 +1,64 @@ +name: Test Template(s) + +on: + workflow_call: + inputs: + template: + required: true + type: string + description: 'Template name (e.g., vite, starter)' + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install dependencies (tests) + working-directory: tests + run: npm ci + + - name: Install dependencies (template) + working-directory: templates/${{ inputs.template }} + run: npm install + + - name: Get Playwright version + id: playwright-version + working-directory: tests + run: echo "version=$(npm ls @playwright/test --json | jq -r '.dependencies["@playwright/test"].version')" >> $GITHUB_OUTPUT + + - name: Cache Playwright browsers + uses: actions/cache@v4 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: playwright-${{ steps.playwright-version.outputs.version }} + + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + working-directory: tests + run: npx playwright install --with-deps chromium + + - name: Install Playwright system dependencies + if: steps.playwright-cache.outputs.cache-hit == 'true' + run: npx playwright install-deps chromium + + - name: Run Playwright tests + working-directory: tests + run: npm run test + env: + CI: true + TEMPLATE: ${{ inputs.template }} + + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report-${{ inputs.template }}-template + path: tests/playwright-report/index.html + retention-days: 30 diff --git a/.github/workflows/test-vite-template.yml b/.github/workflows/test-vite-template.yml new file mode 100644 index 0000000..b398eed --- /dev/null +++ b/.github/workflows/test-vite-template.yml @@ -0,0 +1,19 @@ +name: Test Vite Template + +on: + push: + branches: [main] + paths: + - 'templates/vite/**' + - 'tests/**' + pull_request: + branches: [main] + paths: + - 'templates/vite/**' + - 'tests/**' + +jobs: + test-vite-template: + uses: ./.github/workflows/test-template.yml + with: + template: vite diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..c9b126c --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,7 @@ +# Dependencies +node_modules/ + +# Playwright +playwright-report/ +test-results/ +playwright/.cache/ diff --git a/tests/e2e/starter.spec.ts b/tests/e2e/starter.spec.ts new file mode 100644 index 0000000..d88d16c --- /dev/null +++ b/tests/e2e/starter.spec.ts @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Starter Template', () => { + test('should load the app and display main content', async ({ page }) => { + await page.goto('/'); + + // Check that the page title is correct + await expect(page).toHaveTitle('Power Apps'); + + // Check that the main heading is visible + await expect(page.getByRole('heading', { name: 'Power + Code' })).toBeVisible(); + + // Check that the Power Apps and React logos are present + await expect(page.getByAltText('Power Apps logo')).toBeVisible(); + await expect(page.getByAltText('React logo')).toBeVisible(); + }); + + test('should have working counter button', async ({ page }) => { + await page.goto('/'); + + // Find the counter button and verify initial state + const counterButton = page.getByRole('button', { name: /count is/i }); + await expect(counterButton).toBeVisible(); + await expect(counterButton).toHaveText('count is 0'); + + // Click the button and verify the count increments + await counterButton.click(); + await expect(counterButton).toHaveText('count is 1'); + + // Click again to verify continued functionality + await counterButton.click(); + await expect(counterButton).toHaveText('count is 2'); + }); + + test('should have theme toggle', async ({ page }) => { + await page.goto('/'); + + // Check that the theme toggle button exists + const themeToggle = page.getByRole('button', { name: /toggle theme/i }); + await expect(themeToggle).toBeVisible(); + }); +}); diff --git a/tests/e2e/vite.spec.ts b/tests/e2e/vite.spec.ts new file mode 100644 index 0000000..5487e23 --- /dev/null +++ b/tests/e2e/vite.spec.ts @@ -0,0 +1,34 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Vite React Template', () => { + test('should load the app and display main content', async ({ page }) => { + await page.goto('/'); + + // Check that the page title is correct + await expect(page).toHaveTitle('vite'); + + // Check that the main heading is visible + await expect(page.getByRole('heading', { name: 'Vite + React' })).toBeVisible(); + + // Check that the Vite and React logos are present + await expect(page.getByAltText('Vite logo')).toBeVisible(); + await expect(page.getByAltText('React logo')).toBeVisible(); + }); + + test('should have working counter button', async ({ page }) => { + await page.goto('/'); + + // Find the counter button and verify initial state + const counterButton = page.getByRole('button', { name: /count is/i }); + await expect(counterButton).toBeVisible(); + await expect(counterButton).toHaveText('count is 0'); + + // Click the button and verify the count increments + await counterButton.click(); + await expect(counterButton).toHaveText('count is 1'); + + // Click again to verify continued functionality + await counterButton.click(); + await expect(counterButton).toHaveText('count is 2'); + }); +}); diff --git a/tests/package-lock.json b/tests/package-lock.json new file mode 100644 index 0000000..dc835a0 --- /dev/null +++ b/tests/package-lock.json @@ -0,0 +1,90 @@ +{ + "name": "powerappscodeapps-tests", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "powerappscodeapps-tests", + "version": "0.0.0", + "devDependencies": { + "@playwright/test": "^1.40.0", + "@types/node": "^24.10.9" + } + }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "24.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", + "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "dev": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true + } + } +} diff --git a/tests/package.json b/tests/package.json new file mode 100644 index 0000000..03add6a --- /dev/null +++ b/tests/package.json @@ -0,0 +1,12 @@ +{ + "name": "powerappscodeapps-tests", + "private": true, + "version": "0.1.0", + "scripts": { + "test": "playwright test" + }, + "devDependencies": { + "@playwright/test": "^1.40.0", + "@types/node": "^24.10.9" + } +} diff --git a/tests/playwright.config.ts b/tests/playwright.config.ts new file mode 100644 index 0000000..995e02b --- /dev/null +++ b/tests/playwright.config.ts @@ -0,0 +1,55 @@ +import { defineConfig, devices } from "@playwright/test"; +import path from "path"; + +const isCI = !!process.env.CI; +const template: "vite" | "starter" = process.env.TEMPLATE; // Set to 'vite' or 'starter' to run only one + +const webServers = { + vite: { + name: "vite-template", + command: "npm run build && npm run preview -- --port 4173", + cwd: path.resolve(__dirname, "../templates/vite"), + url: "http://localhost:4173", + reuseExistingServer: !isCI, + }, + starter: { + name: "starter-template", + command: "npm run build && npm run preview -- --port 4174", + cwd: path.resolve(__dirname, "../templates/starter"), + url: "http://localhost:4174", + reuseExistingServer: !isCI, + }, +}; + +const projects = { + vite: { + name: "vite", + testMatch: "vite.spec.ts", + use: { + ...devices["Desktop Chrome"], + baseURL: "http://localhost:4173", + }, + }, + starter: { + name: "starter", + testMatch: "starter.spec.ts", + use: { + ...devices["Desktop Chrome"], + baseURL: "http://localhost:4174", + }, + }, +}; + +export default defineConfig({ + testDir: "./e2e", + fullyParallel: true, + forbidOnly: isCI, + retries: isCI ? 2 : 0, + workers: isCI ? 1 : undefined, + reporter: isCI ? [["github"], ["html", { open: "never" }]] : "html", + use: { + trace: "on-first-retry", + }, + projects: projects[template] ? [projects[template]] : Object.values(projects), + webServer: webServers[template] ?? Object.values(webServers), +});