|
| 1 | +import { expect } from '@playwright/test' |
| 2 | + |
| 3 | +import { externalLinks, getRoutes } from '../src/config/routes' |
| 4 | +import { wanAnimate2Page } from '../src/data/wanAnimate2' |
| 5 | +import { t } from '../src/i18n/translations' |
| 6 | +import { test } from './fixtures/blockExternalMedia' |
| 7 | + |
| 8 | +const PATH = '/wan-animate-2' |
| 9 | +const ZH_PATH = '/zh-CN/wan-animate-2' |
| 10 | +const HERO_TITLE = t('wanAnimate2.hero.title') |
| 11 | +const HERO_EYEBROW = t('wanAnimate2.hero.eyebrow') |
| 12 | +const HERO_CTA = t('wanAnimate2.hero.primaryCta') |
| 13 | +const RUN_OPTIONS_HEADING = t('wanAnimate2.runOptions.heading') |
| 14 | +const REVIEWS_HEADING = t('wanAnimate2.reviews.heading') |
| 15 | +const MODELS_ROUTE = getRoutes('en').models |
| 16 | + |
| 17 | +test.describe('Wan Animate 2 announcement page @smoke', () => { |
| 18 | + test.beforeEach(async ({ page }) => { |
| 19 | + await page.goto(PATH) |
| 20 | + }) |
| 21 | + |
| 22 | + test('renders the hero over its backdrop and is indexable', async ({ |
| 23 | + page |
| 24 | + }) => { |
| 25 | + const hero = page.locator('section').filter({ |
| 26 | + has: page.getByRole('heading', { level: 1, name: HERO_TITLE }) |
| 27 | + }) |
| 28 | + await expect(hero.getByText(HERO_EYEBROW)).toBeVisible() |
| 29 | + await expect( |
| 30 | + page.getByRole('heading', { level: 1, name: HERO_TITLE }) |
| 31 | + ).toBeVisible() |
| 32 | + await expect(hero.locator('img')).toHaveAttribute( |
| 33 | + 'src', |
| 34 | + wanAnimate2Page.hero.placeholderImageSrc ?? '' |
| 35 | + ) |
| 36 | + await expect(page.locator('meta[name="robots"]')).toHaveCount(0) |
| 37 | + }) |
| 38 | + |
| 39 | + test('hero CTA sends visitors to the cloud in a new tab', async ({ |
| 40 | + page |
| 41 | + }) => { |
| 42 | + const cta = page |
| 43 | + .locator('section') |
| 44 | + .filter({ |
| 45 | + has: page.getByRole('heading', { level: 1, name: HERO_TITLE }) |
| 46 | + }) |
| 47 | + .getByRole('link', { name: HERO_CTA }) |
| 48 | + await expect(cta).toHaveAttribute('href', externalLinks.cloud) |
| 49 | + await expect(cta).toHaveAttribute('target', '_blank') |
| 50 | + }) |
| 51 | + |
| 52 | + test('breadcrumb trail links to the models catalog', async ({ page }) => { |
| 53 | + const modelsCrumb = page |
| 54 | + .getByRole('navigation', { name: t('ui.breadcrumb') }) |
| 55 | + .getByRole('link', { name: t('models.breadcrumb.models') }) |
| 56 | + await expect(modelsCrumb).toHaveAttribute('href', MODELS_ROUTE) |
| 57 | + }) |
| 58 | + |
| 59 | + test('footer links back to this page', async ({ page }) => { |
| 60 | + const footerLink = page |
| 61 | + .locator('footer') |
| 62 | + .getByRole('link', { name: t('footer.wanAnimate2') }) |
| 63 | + await expect(footerLink).toHaveAttribute( |
| 64 | + 'href', |
| 65 | + getRoutes('en').wanAnimate2 |
| 66 | + ) |
| 67 | + }) |
| 68 | + |
| 69 | + test('renders run options and reviews', async ({ page }) => { |
| 70 | + const runOptions = page.getByRole('heading', { |
| 71 | + level: 2, |
| 72 | + name: RUN_OPTIONS_HEADING |
| 73 | + }) |
| 74 | + await runOptions.scrollIntoViewIfNeeded() |
| 75 | + await expect(runOptions).toBeVisible() |
| 76 | + |
| 77 | + const reviews = page.getByRole('heading', { |
| 78 | + level: 2, |
| 79 | + name: REVIEWS_HEADING |
| 80 | + }) |
| 81 | + await reviews.scrollIntoViewIfNeeded() |
| 82 | + await expect(reviews).toBeVisible() |
| 83 | + }) |
| 84 | + |
| 85 | + // The announcement omits gallery, pricing, FAQ and closing CTA, so these two |
| 86 | + // are the only sections it should show. Asserting the whole set means |
| 87 | + // reintroducing any of them has to be deliberate. |
| 88 | + test('shows only the sections the announcement configures', async ({ |
| 89 | + page |
| 90 | + }) => { |
| 91 | + await expect(page.getByRole('heading', { level: 2 })).toHaveText([ |
| 92 | + RUN_OPTIONS_HEADING, |
| 93 | + REVIEWS_HEADING |
| 94 | + ]) |
| 95 | + await expect(page.locator('video')).toHaveCount(0) |
| 96 | + }) |
| 97 | +}) |
| 98 | + |
| 99 | +test.describe('Wan Animate 2 announcement page — zh-CN', () => { |
| 100 | + test.beforeEach(async ({ page }) => { |
| 101 | + await page.goto(ZH_PATH) |
| 102 | + }) |
| 103 | + |
| 104 | + test('renders the localized hero and run options', async ({ page }) => { |
| 105 | + // Resolve the heading in zh-CN rather than reusing HERO_TITLE. The two are |
| 106 | + // the same string today because the model name is not translated, but |
| 107 | + // scoping by locale here is what the test actually means. |
| 108 | + const hero = page.locator('section').filter({ |
| 109 | + has: page.getByRole('heading', { |
| 110 | + level: 1, |
| 111 | + name: t('wanAnimate2.hero.title', 'zh-CN') |
| 112 | + }) |
| 113 | + }) |
| 114 | + await expect( |
| 115 | + hero.getByText(t('wanAnimate2.hero.eyebrow', 'zh-CN')) |
| 116 | + ).toBeVisible() |
| 117 | + await expect(hero.getByRole('link')).toContainText(/[一-鿿]/) |
| 118 | + |
| 119 | + await expect( |
| 120 | + page.getByRole('heading', { |
| 121 | + level: 2, |
| 122 | + name: t('wanAnimate2.runOptions.heading', 'zh-CN') |
| 123 | + }) |
| 124 | + ).toBeVisible() |
| 125 | + }) |
| 126 | + |
| 127 | + test('breadcrumb and footer keep visitors inside the locale', async ({ |
| 128 | + page |
| 129 | + }) => { |
| 130 | + const modelsCrumb = page |
| 131 | + .getByRole('navigation', { name: t('ui.breadcrumb', 'zh-CN') }) |
| 132 | + .getByRole('link', { name: t('models.breadcrumb.models', 'zh-CN') }) |
| 133 | + await expect(modelsCrumb).toHaveAttribute('href', getRoutes('zh-CN').models) |
| 134 | + |
| 135 | + const footerLink = page |
| 136 | + .locator('footer') |
| 137 | + .getByRole('link', { name: t('footer.wanAnimate2', 'zh-CN') }) |
| 138 | + await expect(footerLink).toHaveAttribute( |
| 139 | + 'href', |
| 140 | + getRoutes('zh-CN').wanAnimate2 |
| 141 | + ) |
| 142 | + }) |
| 143 | +}) |
| 144 | + |
| 145 | +test.describe('Wan Animate 2 announcement page — mobile @mobile', () => { |
| 146 | + test('hero CTA stays visible and linked at narrow viewports', async ({ |
| 147 | + page |
| 148 | + }) => { |
| 149 | + await page.goto(PATH) |
| 150 | + |
| 151 | + const cta = page |
| 152 | + .locator('section') |
| 153 | + .filter({ |
| 154 | + has: page.getByRole('heading', { level: 1, name: HERO_TITLE }) |
| 155 | + }) |
| 156 | + .getByRole('link', { name: HERO_CTA }) |
| 157 | + |
| 158 | + await expect(cta).toBeVisible() |
| 159 | + await expect(cta).toHaveAttribute('href', externalLinks.cloud) |
| 160 | + |
| 161 | + const box = await cta.boundingBox() |
| 162 | + const viewport = page.viewportSize() |
| 163 | + if (!box || !viewport) { |
| 164 | + throw new Error('hero CTA has no layout box to measure') |
| 165 | + } |
| 166 | + |
| 167 | + // Measured without scrolling first: the point is that the CTA is reachable |
| 168 | + // on load, so scrolling it into view would make the vertical bounds pass no |
| 169 | + // matter how far down the page it sat. One pixel of tolerance for subpixel |
| 170 | + // rounding. |
| 171 | + expect(box.x).toBeGreaterThanOrEqual(-1) |
| 172 | + expect(box.y).toBeGreaterThanOrEqual(-1) |
| 173 | + expect(box.x + box.width).toBeLessThanOrEqual(viewport.width + 1) |
| 174 | + expect(box.y + box.height).toBeLessThanOrEqual(viewport.height + 1) |
| 175 | + }) |
| 176 | +}) |
0 commit comments