Skip to content
Open
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
59 changes: 54 additions & 5 deletions e2e/app.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,64 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';

// Ionic keeps tab pages it has shown in the DOM (hidden), so only look at visible elements.
const visibleTitle = (page: Page) => page.locator('ion-title').filter({ visible: true }).first();
// The outgoing page can still be visible mid-transition, so match the content by its page name.
const visibleContent = (page: Page, pageName: string) =>
page.locator('ion-content').filter({ visible: true, hasText: pageName });

const tabs = {
tab1: { name: 'Tab 1', url: /\/tab1$/, title: 'Tab 1' },
photos: { name: 'Photos', url: /\/tab2$/, title: 'Photo Gallery' },
tab3: { name: 'Tab 3', url: /\/tab3$/, title: 'Tab 3' },
};

test('shows the first tab on load', async ({ page }) => {
await page.goto('/');

await expect(page.locator('ion-content').getByText('Tab 1 page')).toBeVisible();
await expect(page).toHaveURL(tabs.tab1.url);
await expect(visibleTitle(page)).toHaveText(tabs.tab1.title);
await expect(page.getByRole('tab', { name: tabs.tab1.name })).toHaveAttribute('aria-selected', 'true');
});

test('switches tabs from the tab bar', async ({ page }) => {
await page.goto('/');

for (const tab of [tabs.photos, tabs.tab3, tabs.tab1]) {
await page.getByRole('tab', { name: tab.name }).click();

await expect(page).toHaveURL(tab.url);
await expect(visibleTitle(page)).toHaveText(tab.title);
await expect(page.getByRole('tab', { name: tab.name })).toHaveAttribute('aria-selected', 'true');
}
});

test('opens the photo gallery from the tab bar', async ({ page }) => {
test('shows an icon on each tab', async ({ page }) => {
await page.goto('/');
await page.getByRole('tab', { name: 'Photos' }).click();

await expect(page).toHaveURL(/\/tab2$/);
await expect(page.locator('ion-title', { hasText: 'Photo Gallery' }).first()).toBeVisible();
for (const tab of [tabs.tab1, tabs.photos, tabs.tab3]) {
// ion-icon draws its SVG in its shadow root, which Playwright's locators can see.
const tabButton = page.locator('ion-tab-button').filter({ hasText: tab.name });
await expect(tabButton.locator('ion-icon svg')).toBeVisible();
}
});

for (const tab of [
{ ...tabs.tab1, pageName: 'Tab 1 page' },
{ ...tabs.tab3, pageName: 'Tab 3 page' },
]) {
test(`${tab.name} links to the Ionic UI components docs`, async ({ page }) => {
await page.goto('/');
await page.getByRole('tab', { name: tab.name }).click();
await expect(page).toHaveURL(tab.url);

const content = visibleContent(page, tab.pageName);
await expect(content.getByText(tab.pageName, { exact: true })).toBeVisible();
await expect(content).toContainText('Explore UI Components');

const link = content.getByRole('link', { name: 'UI Components' });
await expect(link).toHaveAttribute('href', 'https://ionicframework.com/docs/components');
await expect(link).toHaveAttribute('target', '_blank');
await expect(link).toHaveAttribute('rel', 'noopener noreferrer');
});
}
93 changes: 93 additions & 0 deletions e2e/gallery.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';

// Chromium's fake camera stands in for a real one, so these tests run the actual
// Capacitor Camera web flow (the pwa-elements camera modal).
test.use({
permissions: ['camera'],
launchOptions: { args: ['--use-fake-ui-for-media-stream', '--use-fake-device-for-media-stream'] },
});

const takePhoto = async (page: Page): Promise<void> => {
await page.locator('ion-fab-button').click();
await page.locator('pwa-camera .shutter').click();
await page.locator('pwa-camera .accept-use').click();
};

const photo = (page: Page, position: number) => page.getByRole('button', { name: `Photo ${position}` });

test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.getByRole('tab', { name: 'Photos' }).click();
});

test('takes a photo and adds it to the gallery', async ({ page }) => {
await takePhoto(page);

await expect(photo(page, 1)).toBeVisible();
await expect(photo(page, 2)).toHaveCount(0);
});

test('keeps photos after the app reloads', async ({ page }) => {
await takePhoto(page);
await expect(photo(page, 1)).toBeVisible();

await page.reload();

await expect(page.getByRole('img', { name: 'Photo 1' })).toHaveAttribute('src', /^data:image\/jpeg;base64,/);
});

test('deletes a photo from the action sheet', async ({ page }) => {
await takePhoto(page);
await takePhoto(page);
await expect(photo(page, 2)).toBeVisible();

await photo(page, 1).click();
await page.getByRole('button', { name: 'Delete' }).click();

await expect(photo(page, 2)).toHaveCount(0);
await expect(photo(page, 1)).toBeVisible();

await page.reload();

// Wait for the remaining photo to load from storage before checking the deleted one stayed gone.
await expect(page.getByRole('img', { name: 'Photo 1' })).toHaveAttribute('src', /^data:image\/jpeg;base64,/);
await expect(photo(page, 2)).toHaveCount(0);
});

test('adds a new photo after deleting one', async ({ page }) => {
await takePhoto(page);
await photo(page, 1).click();
await page.getByRole('button', { name: 'Delete' }).click();
await expect(photo(page, 1)).toHaveCount(0);

await takePhoto(page);

await expect(photo(page, 1)).toBeVisible();
await expect(photo(page, 2)).toHaveCount(0);

await page.reload();

// Wait for the saved photo to load from storage before checking it's the only one.
await expect(page.getByRole('img', { name: 'Photo 1' })).toHaveAttribute('src', /^data:image\/jpeg;base64,/);
await expect(photo(page, 2)).toHaveCount(0);
});

test('keeps the photo when the action sheet is cancelled', async ({ page }) => {
await takePhoto(page);
await photo(page, 1).click();
await page.getByRole('button', { name: 'Cancel' }).click();

await expect(page.locator('ion-action-sheet')).toBeHidden();
await expect(photo(page, 1)).toBeVisible();

// The photo's actions open again after a cancel.
await photo(page, 1).click();
await expect(page.getByRole('button', { name: 'Delete' })).toBeVisible();
});

test('shows a camera icon on the camera button', async ({ page }) => {
// ion-icon draws its SVG in its shadow root, which Playwright's locators can see. Only match
// the button's own icon, not the close icon ion-fab-button keeps in its shadow root.
await expect(page.locator('ion-fab-button > ion-icon').locator('svg')).toBeVisible();
});
8 changes: 4 additions & 4 deletions src/components/ExploreContainer.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.container {
#container {
text-align: center;
position: absolute;
left: 0;
Expand All @@ -7,18 +7,18 @@
transform: translateY(-50%);
}

.container strong {
#container strong {
font-size: 20px;
line-height: 26px;
}

.container p {
#container p {
font-size: 16px;
line-height: 22px;
color: #8c8c8c;
margin: 0;
}

.container a {
#container a {
text-decoration: none;
}
2 changes: 1 addition & 1 deletion src/components/ExploreContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ interface ContainerProps {

const ExploreContainer: React.FC<ContainerProps> = ({ name }) => {
return (
<div className="container">
<div id="container">
<strong>{name}</strong>
<p>
Explore{' '}
Expand Down
Loading
Loading