From 60e5a93832e055414ce5b1ef4ca78842987b05e9 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 1 Mar 2023 08:49:31 -0800 Subject: [PATCH] fix(store): support text and binary values (#21006) --- .../playwright-core/src/utils/mimeType.ts | 4 + packages/playwright-test/src/store.ts | 42 ++++++++- tests/playwright-test/store.spec.ts | 93 ++++++++++++++----- 3 files changed, 112 insertions(+), 27 deletions(-) diff --git a/packages/playwright-core/src/utils/mimeType.ts b/packages/playwright-core/src/utils/mimeType.ts index 388bd6cb2e..2f8b9d4829 100644 --- a/packages/playwright-core/src/utils/mimeType.ts +++ b/packages/playwright-core/src/utils/mimeType.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +export function isJsonMimeType(mimeType: string) { + return !!mimeType.match(/^(application\/json|application\/.*?\+json|text\/(x-)?json)(;\s*charset=.*)?$/); +} + export function isTextualMimeType(mimeType: string) { return !!mimeType.match(/^(text\/.*?|application\/(json|(x-)?javascript|xml.*?|ecmascript|graphql|x-www-form-urlencoded)|image\/svg(\+xml)?|application\/.*?(\+json|\+xml))(;\s*charset=.*)?$/); } \ No newline at end of file diff --git a/packages/playwright-test/src/store.ts b/packages/playwright-test/src/store.ts index a0e50598b1..36eef5d8b3 100644 --- a/packages/playwright-test/src/store.ts +++ b/packages/playwright-test/src/store.ts @@ -18,6 +18,8 @@ import fs from 'fs'; import path from 'path'; import type { TestStore } from '../types/test'; import { currentConfig } from './common/globals'; +import { mime } from 'playwright-core/lib/utilsBundle'; +import { isJsonMimeType, isString, isTextualMimeType } from 'playwright-core/lib/utils'; class JsonStore implements TestStore { async delete(name: string) { @@ -28,8 +30,13 @@ class JsonStore implements TestStore { async get(name: string) { const file = this.path(name); try { - const data = await fs.promises.readFile(file, 'utf-8'); - return JSON.parse(data) as T; + const type = contentType(name); + if (type === 'binary') + return await fs.promises.readFile(file) as T; + const text = await fs.promises.readFile(file, 'utf-8'); + if (type === 'json') + return JSON.parse(text) as T; + return text as T; } catch (e) { return undefined; } @@ -52,10 +59,39 @@ class JsonStore implements TestStore { await fs.promises.rm(file, { force: true }); return; } - const data = JSON.stringify(value, undefined, 2); + let data: string | Buffer = ''; + switch (contentType(name)) { + case 'json': { + if (Buffer.isBuffer(value)) + throw new Error('JSON value must be an Object'); + data = JSON.stringify(value, undefined, 2); + break; + } + case 'text': { + if (!isString(value)) + throw new Error('Textual value must be a string'); + data = value as string; + break; + } + case 'binary': { + if (!Buffer.isBuffer(value)) + throw new Error('Binary value must be a Buffer'); + data = value; + break; + } + } await fs.promises.mkdir(path.dirname(file), { recursive: true }); await fs.promises.writeFile(file, data); } } +function contentType(name: string): 'json'|'text'|'binary' { + const mimeType = mime.getType(path.basename(name)) ?? 'application/octet-string'; + if (isJsonMimeType(mimeType)) + return 'json'; + if (isTextualMimeType(mimeType)) + return 'text'; + return 'binary'; +} + export const store = new JsonStore(); diff --git a/tests/playwright-test/store.spec.ts b/tests/playwright-test/store.spec.ts index e18585d069..0de660f37d 100644 --- a/tests/playwright-test/store.spec.ts +++ b/tests/playwright-test/store.spec.ts @@ -27,15 +27,15 @@ test('should provide store fixture', async ({ runInlineTest }) => { import { test, store, expect } from '@playwright/test'; test('should store number', async ({ }) => { expect(store).toBeTruthy(); - expect(await store.get('number')).toBe(undefined); - await store.set('number', 2022) - expect(await store.get('number')).toBe(2022); + expect(await store.get('number.json')).toBe(undefined); + await store.set('number.json', 2022) + expect(await store.get('number.json')).toBe(2022); }); test('should store object', async ({ }) => { expect(store).toBeTruthy(); - expect(await store.get('object')).toBe(undefined); - await store.set('object', { 'a': 2022 }) - expect(await store.get('object')).toEqual({ 'a': 2022 }); + expect(await store.get('object.json')).toBe(undefined); + await store.set('object.json', { 'a': 2022 }) + expect(await store.get('object.json')).toEqual({ 'a': 2022 }); }); `, }, { workers: 1 }); @@ -63,27 +63,27 @@ test('should share store state between project setup and tests', async ({ runInl 'store.setup.ts': ` import { test, store, expect } from '@playwright/test'; test('should initialize store', async ({ }) => { - expect(await store.get('number')).toBe(undefined); - await store.set('number', 2022) - expect(await store.get('number')).toBe(2022); + expect(await store.get('number.json')).toBe(undefined); + await store.set('number.json', 2022) + expect(await store.get('number.json')).toBe(2022); - expect(await store.get('object')).toBe(undefined); - await store.set('object', { 'a': 2022 }) - expect(await store.get('object')).toEqual({ 'a': 2022 }); + expect(await store.get('object.json')).toBe(undefined); + await store.set('object.json', { 'a': 2022 }) + expect(await store.get('object.json')).toEqual({ 'a': 2022 }); }); `, 'a.test.ts': ` import { test, store, expect } from '@playwright/test'; test('should get data from setup', async ({ }) => { - expect(await store.get('number')).toBe(2022); - expect(await store.get('object')).toEqual({ 'a': 2022 }); + expect(await store.get('number.json')).toBe(2022); + expect(await store.get('object.json')).toEqual({ 'a': 2022 }); }); `, 'b.test.ts': ` import { test, store, expect } from '@playwright/test'; test('should get data from setup', async ({ }) => { - expect(await store.get('number')).toBe(2022); - expect(await store.get('object')).toEqual({ 'a': 2022 }); + expect(await store.get('number.json')).toBe(2022); + expect(await store.get('object.json')).toEqual({ 'a': 2022 }); }); `, }, { workers: 1 }); @@ -99,17 +99,17 @@ test('should persist store state between project runs', async ({ runInlineTest } 'a.test.ts': ` import { test, store, expect } from '@playwright/test'; test('should have no data on first run', async ({ }) => { - expect(await store.get('number')).toBe(undefined); - await store.set('number', 2022) - expect(await store.get('object')).toBe(undefined); - await store.set('object', { 'a': 2022 }) + expect(await store.get('number.json')).toBe(undefined); + await store.set('number.json', 2022) + expect(await store.get('object.json')).toBe(undefined); + await store.set('object.json', { 'a': 2022 }) }); `, 'b.test.ts': ` import { test, store, expect } from '@playwright/test'; test('should get data from previous run', async ({ }) => { - expect(await store.get('number')).toBe(2022); - expect(await store.get('object')).toEqual({ 'a': 2022 }); + expect(await store.get('number.json')).toBe(2022); + expect(await store.get('object.json')).toEqual({ 'a': 2022 }); }); `, }; @@ -152,13 +152,13 @@ test('should load context storageState from store', async ({ runInlineTest, serv expect(await store.get('user')).toBe(undefined); await page.goto('${server.PREFIX}/setcookie.html'); const state = await page.context().storageState(); - await store.set('user', state); + await store.set('user.json', state); }); `, 'a.test.ts': ` import { test, store, expect } from '@playwright/test'; test.use({ - storageState: async ({}, use) => use(store.get('user')) + storageState: async ({}, use) => use(store.get('user.json')) }) test('should get data from setup', async ({ page }) => { await page.goto('${server.EMPTY_PAGE}'); @@ -290,3 +290,48 @@ test('should delete value', async ({ runInlineTest }) => { expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); }); + +test('should support text, json and binary values', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, store, expect } from '@playwright/test'; + test('json', async ({ }) => { + await store.set('key.json', {'a': 2023}); + expect(await store.get('key.json')).toEqual({ 'a': 2023 }); + }); + test('text', async ({ }) => { + await store.set('key.txt', 'Hello'); + expect(await store.get('key.txt')).toEqual('Hello'); + }); + test('binary', async ({ }) => { + const buf = Buffer.alloc(256); + for (let i = 0; i < 256; i++) + buf[i] = i; + await store.set('key.png', buf); + expect(await store.get('key.png')).toEqual(buf); + }); + `, + }, { workers: 1 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); +}); + +test('should throw on unsupported value type for given key extension', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, store, expect } from '@playwright/test'; + test('json', async ({ }) => { + const buf = Buffer.alloc(5); + await store.set('key.json', buf); + }); + test('text', async ({ }) => { + await store.set('key.txt', {}); + }); + test('binary', async ({ }) => { + await store.set('key.png', {}); + }); + `, + }, { workers: 1 }); + expect(result.exitCode).toBe(1); + expect(result.failed).toBe(3); +});