import { expect, test } from './fixtures.js' for (const core of ['published', 'candidate']) { test(`${core}: real playback, pause, seek, switch and destroy`, async ({ page, request, diagnostics, browserName }, testInfo) => { const caseId = encodeURIComponent(testInfo.testId) await page.goto(`/test/player.html?core=${core}`) await page.evaluate(url => window.createPlayer(url, true), `/test/pattern.mp4?case=${caseId}`) await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true) expect(await page.locator('video').evaluate(video => video.duration)).toBeCloseTo(8, 1) await expect(page.locator('.art-chapter')).toHaveCount(1) await page.locator('#play').click() await expect.poll(() => page.locator('video').evaluate(video => video.currentTime)).toBeGreaterThan(0.25) // Read decoded frame colors, independent of WebKit Windows' layout-sized videoWidth. const pixels = await page.locator('video').evaluate((video) => { const canvas = document.createElement('canvas') canvas.width = 80 canvas.height = 45 const ctx = canvas.getContext('2d') ctx.drawImage(video, 0, 0, 80, 45) return [30, 45, 75].map(x => [...ctx.getImageData(x, 2, 1, 1).data]) }) expect(pixels[0][0]).toBeGreaterThan(180) expect(pixels[0][1]).toBeGreaterThan(180) expect(pixels[0][2]).toBeLessThan(80) expect(pixels[1][0]).toBeLessThan(80) expect(pixels[1][2]).toBeGreaterThan(180) expect(pixels[2][1]).toBeGreaterThan(180) expect(pixels[2][2]).toBeGreaterThan(180) const mediaRequests = await (await request.get(`/test/requests.json?case=${caseId}`)).json() expect(mediaRequests.some(item => item.path === '/test/pattern.mp4')).toBe(true) await testInfo.attach('decoded-frame', { body: JSON.stringify({ pixels, mediaRequests }), contentType: 'application/json' }) await page.locator('#pause').click() await expect.poll(() => page.locator('video').evaluate(video => video.paused)).toBe(true) const pausedTime = await page.locator('video').evaluate(video => video.currentTime) // A bounded observation interval checks that pause stops the media clock. await page.waitForTimeout(200) expect(await page.locator('video').evaluate(video => video.currentTime)).toBeCloseTo(pausedTime, 2) await page.evaluate(() => { window.art.seek = 3 }) await expect.poll(() => page.evaluate(() => window.events.includes('video:seeked'))).toBe(true) expect(await page.locator('video').evaluate(video => video.currentTime)).toBeCloseTo(3, 1) // Reuse a real docs sample, independently of the short synthetic fixture. await page.evaluate(url => window.art.switchUrl(url), `/assets/sample/video.mp4?case=${caseId}`) await expect.poll(() => page.locator('video').evaluate(video => video.duration)).toBeGreaterThan(90) await page.locator('#play').click() await expect.poll(() => page.locator('video').evaluate(video => video.currentTime)).toBeGreaterThan(0.25) const events = await page.evaluate(() => window.events) expect(events).toContain('video:loadedmetadata') expect(events).toContain('ready') expect(events.indexOf('video:loadedmetadata')).toBeLessThan(events.indexOf('ready')) expect(events).toContain('video:playing') expect(events).not.toContain('video:error') await page.evaluate(() => window.art.destroy()) await expect(page.locator('.player')).toBeEmpty() expect(await page.evaluate(() => window.Artplayer.instances.length)).toBe(0) expect(diagnostics.consoleErrors).toEqual([]) // Engines cancel media range requests as buffering, seeking and source ownership change. // Only exact cancellations of this test's two media URLs are expected; retain all in evidence. const unexpectedRequests = diagnostics.failedRequests.filter((item) => { const url = new URL(item.url) const expectedCancellation = (browserName === 'chromium' && item.failure?.errorText === 'net::ERR_ABORTED') || (browserName === 'firefox' && item.failure?.errorText === 'NS_ERROR_PARSED_DATA_CACHED') return !(item.resourceType === 'media' && expectedCancellation && url.origin === new URL(page.url()).origin && url.searchParams.get('case') === testInfo.testId && ['/test/pattern.mp4', '/assets/sample/video.mp4'].includes(url.pathname)) }) expect(unexpectedRequests).toEqual([]) }) } test('controlled HTTP media failure emits a real video error', async ({ page, request }, testInfo) => { const caseId = encodeURIComponent(testInfo.testId) await page.goto('/test/player.html') await page.evaluate(url => window.createPlayer(url), `/test/fail.mp4?case=${caseId}`) await expect.poll(() => page.evaluate(() => window.events.includes('video:error'))).toBe(true) const requests = await (await request.get(`/test/requests.json?case=${caseId}`)).json() expect(requests.some(item => item.path === '/test/fail.mp4')).toBe(true) expect((await request.get('/test/fail.mp4')).status()).toBe(503) expect(await page.locator('video').evaluate(video => video.error.code)).toBeGreaterThan(0) await page.evaluate(() => window.art.destroy()) await expect(page.locator('.player')).toBeEmpty() }) test('Range, media identity and docs candidate routes', async ({ request }) => { const manifest = await (await request.get('/test/manifest.json')).json() const candidate = await request.get('/candidate/artplayer.js') const alias = await request.get('/uncompiled/artplayer/index.js') expect(await alias.body()).toEqual(await candidate.body()) expect(manifest.resources['/uncompiled/artplayer/index.js'].aliasOf).toBe('/candidate/artplayer.js') const full = await (await request.get('/test/pattern.mp4')).body() for (const [range, start, end] of [['bytes=0-31', 0, 32], ['bytes=-16', full.length - 16, full.length], ['bytes=32-', 32, full.length]]) { const response = await request.get('/test/pattern.mp4', { headers: { Range: range } }) expect(response.status()).toBe(206) expect(await response.body()).toEqual(full.subarray(start, end)) } expect((await request.get('/test/pattern.mp4', { headers: { Range: 'bytes=999999999-' } })).status()).toBe(416) expect((await request.get('/compiled/missing.js')).status()).toBe(404) expect((await request.get('/mobile.html')).status()).toBe(200) expect((await request.get('/?libs=./uncompiled/artplayer-plugin-chapter/index.js&example=chapter')).status()).toBe(200) })