Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,36 @@ concurrency:
cancel-in-progress: true

jobs:
sync:
name: Sync Main to GH-Pages
uses: ./.github/workflows/sync-gh-pages.yml
guard:
name: Enforce main-only dispatch
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Verify ref is refs/heads/main
run: |
if [ "${{ github.ref }}" != "refs/heads/main" ]; then
echo "::error::Release can only be dispatched from 'main' (got ${{ github.ref }})."
exit 1
fi

test-website:
name: Website Testing
needs: guard
uses: ./.github/workflows/website-test.yml
permissions:
contents: write
contents: read

deploy:
name: Deploy Site To GitHub Pages
needs: sync
needs: test-website
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ needs.sync.outputs.synced_sha }}
ref: ${{ github.sha }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
Expand All @@ -42,14 +55,10 @@ jobs:
run: |
npm ci
npm run build
- name: Test
working-directory: website
run: |
npm test
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: ./website/build
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
22 changes: 0 additions & 22 deletions .github/workflows/sync-gh-pages.yml

This file was deleted.

32 changes: 0 additions & 32 deletions .github/workflows/test-deploy.yml

This file was deleted.

77 changes: 77 additions & 0 deletions .github/workflows/website-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Website Testing

# Runs on PRs and pushes to validate the website, and is called from
# release.yml as the pre-deploy gate.
#
# All events run: npm ci -> unit tests -> build.
# Release dispatch / manual dispatch additionally run Playwright e2e.
# PR / push runs skip the heavier browser suite to keep feedback fast.
on:
pull_request:
branches:
- main
paths:
- 'website/**'
- '.github/workflows/website-test.yml'
push:
branches:
- main
paths:
- 'website/**'
- '.github/workflows/website-test.yml'
workflow_call:
workflow_dispatch:

permissions:
contents: read

jobs:
test-website:
name: Test website
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.sha }}

- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
Comment thread
hemarina marked this conversation as resolved.
with:
node-version: 24
cache: npm
cache-dependency-path: website/package-lock.json

- name: Install dependencies
working-directory: website
run: npm ci

- name: Run unit tests
working-directory: website
run: npm test

- name: Build website
working-directory: website
run: npm run build

# ----- e2e steps below: only on release dispatch and manual runs -----

- name: Install Playwright browsers
if: ${{ github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }}
working-directory: website
run: npx playwright install chromium --with-deps

- name: Run Playwright tests
if: ${{ github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }}
working-directory: website
run: npx playwright test

- name: Upload Playwright report
if: ${{ failure() && (github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch') }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report
path: website/playwright-report/
retention-days: 7
11 changes: 11 additions & 0 deletions website/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,14 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Playwright
/playwright-report
/test-results
/playwright/.cache

# Override the root `*.png` rule for the website tree. PNGs under
# website/static and any future e2e screenshot baselines should be
# tracked; PNGs inside the ignored directories above remain ignored
# (parent-dir ignore wins over file-level re-include).
!*.png
5 changes: 4 additions & 1 deletion website/e2e/gallery-filters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,17 @@ test.describe("Gallery Filtering", () => {
test("empty state shows actionable message when no results", async ({ page }) => {
const searchInput = page.getByRole("searchbox");
await searchInput.fill("zzzznonexistenttemplate");
await searchInput.press("Enter");
await expect(page.locator("text=No templates found")).toBeVisible({ timeout: 10_000 });
});

test("clear filters resets the view", async ({ page }) => {
const searchInput = page.getByRole("searchbox");
await searchInput.fill("python");
await searchInput.press("Enter");
await expect(page).toHaveURL(/name=python/);
await searchInput.clear();
await searchInput.fill("");
await searchInput.press("Enter");
await expect(page).not.toHaveURL(/name=python/);
});
});
146 changes: 146 additions & 0 deletions website/e2e/gallery-functionality.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { test, expect } from "@playwright/test";

// End-to-end coverage modeled on Azure/ai-app-templates' website.spec.ts,
// adapted for awesome-azd's paginated gallery (20 cards/page). Assertions
// against the filtered template *total* read the live "Viewing N templates"
// status text, since the rendered `.fui-Card` count is capped by pagination.
test.describe("Gallery functionality (deploy gate)", () => {
test.beforeEach(async ({ page }) => {
await page.goto("./");
await page.waitForLoadState("networkidle");
await page.waitForSelector(".fui-Card", { timeout: 15_000 });
});

// Reads the total from the "Viewing N templates" live region. The page
// has multiple aria-live polite regions (color-mode toggle, copy toast,
// status counter) so we filter by the literal "Viewing" text.
//
// The status string takes three forms depending on totalItems:
// "Viewing 0 templates" (no results)
// "Viewing 1 template" (single result)
// "Viewing 1-20 of 290 templates" (paginated, > 1 result)
// In every case the LAST integer is the total.
async function viewingCount(page: import("@playwright/test").Page): Promise<number> {
const status = page
.locator('[role="status"]')
.filter({ hasText: /Viewing/ })
.first();
await expect(status).toBeVisible();
const text = (await status.textContent()) ?? "";
const nums = text.match(/\d+/g);
expect(nums, `expected numbers in status: "${text}"`).not.toBeNull();
return Number(nums![nums!.length - 1]);
}

test("homepage renders without 404 / error fallback", async ({ page }) => {
const body = (await page.textContent("body")) ?? "";
expect(body).not.toContain("Page Not Found");
expect(body).not.toContain("404 Not Found");
await expect(page.locator("body")).toBeVisible();
});

test("gallery shows a healthy number of templates", async ({ page }) => {
// Deploy-gate guardrail: if the data pipeline regresses or the gallery
// fails to hydrate, the live counter drops to ~0. A real release has 100+.
const total = await viewingCount(page);
console.log(`Initial gallery total: ${total}`);
expect(total).toBeGreaterThanOrEqual(50);
});

test("page renders at least one template card", async ({ page }) => {
const cards = page.locator(".fui-Card");
const rendered = await cards.count();
expect(rendered).toBeGreaterThan(0);
});

test('search "azure" narrows results but keeps some', async ({ page }) => {
const initial = await viewingCount(page);
expect(initial).toBeGreaterThan(0);

const searchInput = page.locator("#filterBar");
await searchInput.click();
await searchInput.fill("azure");
// The SearchBox's onSearch (URL update + filtering) only fires on Enter.
await searchInput.press("Enter");

await expect(page).toHaveURL(/name=azure/);
await expect
.poll(async () => viewingCount(page), { timeout: 10_000 })
.not.toBe(initial);

const filtered = await viewingCount(page);
console.log(`Gallery total after searching "azure": ${filtered}`);
expect(filtered).toBeGreaterThan(0);
expect(filtered).toBeLessThanOrEqual(initial);
});

test("nonsense search shows the empty-state message", async ({ page }) => {
const searchInput = page.locator("#filterBar");
await searchInput.click();
await searchInput.fill("zzzznonexistenttemplate");
await searchInput.press("Enter");

await expect(page.locator("text=No templates found")).toBeVisible({
timeout: 10_000,
});
});

test("section filter (Language → Python) narrows the gallery", async ({
page,
}) => {
const initial = await viewingCount(page);
expect(initial).toBeGreaterThan(0);

// The left-side filter panel uses checkboxes; clicking one applies a
// tag filter and updates the URL with ?tags=...
// Use the deterministic Fluent UI Checkbox id from ShowcaseLeftFilters
// (`showcase_checkbox_id_<tag>`) to avoid matching "Python" mentions
// inside template card titles, descriptions, or tag chips.
// ShowcaseLeftFilters has two levels of accordions: an outer one per
// section (Language, Framework, etc.) and an inner "View All" that
// hides tags after the first 6. Python is in the Language section, so
// expand that section first, then expand "View All" if needed.
const pythonCheckbox = page.locator("#showcase_checkbox_id_python");
if (!(await pythonCheckbox.isVisible().catch(() => false))) {
await page.getByRole("button", { name: /^Language/ }).first().click();
}
if (!(await pythonCheckbox.isVisible().catch(() => false))) {
const viewAllButtons = page.getByRole("button", { name: /view all/i });
const count = await viewAllButtons.count();
for (let i = 0; i < count; i++) {
await viewAllButtons.nth(i).click();
}
}
await expect(pythonCheckbox).toBeVisible({ timeout: 10_000 });
await pythonCheckbox.click();

await expect(page).toHaveURL(/tags=/);
await expect
.poll(async () => viewingCount(page), { timeout: 10_000 })
.not.toBe(initial);

const filtered = await viewingCount(page);
console.log(`Gallery total after filtering by Python: ${filtered}`);
expect(filtered).toBeGreaterThan(0);
expect(filtered).toBeLessThan(initial);
});

test("clearing search restores the full gallery", async ({ page }) => {
const initial = await viewingCount(page);

const searchInput = page.locator("#filterBar");
await searchInput.fill("python");
await searchInput.press("Enter");
await expect(page).toHaveURL(/name=python/);
await expect
.poll(async () => viewingCount(page), { timeout: 10_000 })
.not.toBe(initial);

await searchInput.fill("");
await searchInput.press("Enter");
await expect(page).not.toHaveURL(/name=python/);
await expect
.poll(async () => viewingCount(page), { timeout: 10_000 })
.toBe(initial);
});
});
8 changes: 5 additions & 3 deletions website/e2e/getting-started.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import { test, expect } from "@playwright/test";
test.describe("Getting Started Page", () => {
test("renders hero section", async ({ page }) => {
await page.goto("getting-started");
await expect(page.locator("h1")).toContainText("Ship to Azure", { timeout: 15_000 });
await page.waitForLoadState("networkidle");
await expect(page.locator("h1")).toContainText("Ship in minutes", { timeout: 15_000 });
});

test("displays three onboarding steps", async ({ page }) => {
await page.goto("getting-started");
await page.waitForLoadState("networkidle");
await expect(page.getByRole("heading", { name: "Install the Azure Developer CLI" })).toBeVisible({ timeout: 15_000 });
await expect(page.getByRole("heading", { name: "Pick a template" })).toBeVisible();
await expect(page.getByRole("heading", { name: "Deploy to Azure" })).toBeVisible();
await expect(page.getByRole("heading", { name: "Pick a template", exact: true })).toBeVisible();
await expect(page.getByRole("heading", { name: "Deploy to Azure", exact: true })).toBeVisible();
});

test("shows step cards section", async ({ page }) => {
Expand Down
Loading