[Bug]: (Playwright-ct) Unexpected Value error for function type for HooksConfig (Pinia Store)
Version
1.47.0
Steps to reproduce
- Clone the minimal reproduced repo : https://github.com/nerdrun/playwright-unexpected-value-issue
- Run
pnpm iandpnpm test-ct - Check the
Unexpected Valueerror message in the console
Expected behavior
Expected behaviour: Any type should be serialized when HooksConfig value is passed to mount(), however innerSerializeValue() doesn't serialize function type field.
store.ts
export type User = {
name: string;
age: number;
toUserString: () => string; // <== function type
};
export const useStore = defineStore('store', () => {
const user = ref<User>({ name: 'Steve', age: 20, toUserString: () => '' });
return {
user,
};
});
index.ts
import { createTestingPinia } from '@pinia/testing';
import { beforeMount } from '@playwright/experimental-ct-vue/hooks';
import { type StoreState } from 'pinia';
import { useStore } from '@/stores/store';
export type HooksConfig = {
store?: StoreState<ReturnType<typeof useStore>>;
};
beforeMount<HooksConfig>(async ({ app, hooksConfig }) => {
const pinia = createTestingPinia({
initialState: {
store: hooksConfig?.store,
},
stubActions: false,
createSpy(args) {
return () => args;
},
});
app.use(pinia);
});
TestPage.ct.ts
import { test, expect } from '@playwright/experimental-ct-vue';
import { type HooksConfig } from 'playwright';
import { type User } from '@/stores/store';
import TestPage from './TestPage.vue';
test.describe('<TestPage />', () => {
test('Test User', async ({ mount }) => {
const mockTestUser: User = {
name: 'Jackson',
age: 30,
toUserString: () => {
return 'Hello';
},
};
const component = await mount<HooksConfig>(TestPage, {
hooksConfig: { store: { user: mockTestUser } },
});
const name = component.getByTestId('name');
await expect(name).toHaveText('Jackson');
});
});
Actual behavior
Throw error message Unexpected value'
Additional context
I've debugged this issue and found out the part that caused the issue.
There is a innerSerializeValue() that serializes arguments that is passed via mount() in
...PATH/node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/playwright-core/lib/protocol/serializers.js
There is no code to handle function in the conditions.
function innerSerializeValue(value, handleSerializer, visitorInfo) {
const handle = handleSerializer(value);
if ('fallThrough' in handle) value = handle.fallThrough;else return handle;
if (typeof value === 'symbol') return {
v: 'undefined'
};
if (Object.is(value, undefined)) return {
v: 'undefined'
};
if (Object.is(value, null)) return {
v: 'null'
};
if (Object.is(value, NaN)) return {
v: 'NaN'
};
if (Object.is(value, Infinity)) return {
v: 'Infinity'
};
if (Object.is(value, -Infinity)) return {
v: '-Infinity'
};
if (Object.is(value, -0)) return {
v: '-0'
};
if (typeof value === 'boolean') return {
b: value
};
if (typeof value === 'number') return {
n: value
};
if (typeof value === 'string') return {
s: value
};
if (typeof value === 'bigint') return {
bi: value.toString()
};
if (isError(value)) return {
e: {
n: value.name,
m: value.message,
s: value.stack || ''
}
};
if (isDate(value)) return {
d: value.toJSON()
};
if (isURL(value)) return {
u: value.toJSON()
};
if (isRegExp(value)) return {
r: {
p: value.source,
f: value.flags
}
};
const id = visitorInfo.visited.get(value);
if (id) return {
ref: id
};
if (Array.isArray(value)) {
const a = [];
const id = ++visitorInfo.lastId;
visitorInfo.visited.set(value, id);
for (let i = 0; i < value.length; ++i) a.push(innerSerializeValue(value[i], handleSerializer, visitorInfo));
return {
a,
id
};
}
if (typeof value === 'object') {
const o = [];
const id = ++visitorInfo.lastId;
visitorInfo.visited.set(value, id);
for (const name of Object.keys(value)) o.push({
k: name,
v: innerSerializeValue(value[name], handleSerializer, visitorInfo)
});
return {
o,
id
};
}
throw new Error('Unexpected value');
}
Environment
System:
OS: macOS 14.6.1
CPU: (12) arm64 Apple M2 Pro
Memory: 81.16 MB / 16.00 GB
Binaries:
Node: 20.16.0 - ~/.asdf/installs/nodejs/20.16.0/bin.node
npm: 10.8.1 - ~/.asdf/plugins/nodejs/shims/npm
pnpm: 9.10.0 - /opt/homebrew/bin/pnpm
Languages:
Bash: 3.2.57 - /bin/bash
npmPackages:
@playwright/experimental-ct-vue: ^1.47.0 => 1.47.0
+1 Did you ever figure out a workaround here?
+1
Playwright executes the test in Node and the component in the browser. During this process, mount props are serialized and transmitted as JSON from Node to the browser. This architecture has several advantages but also introduces certain limitations.
As a workaround, the store state can be made Partial or DeepPartial, with functions simply not being passed:
// playwright/index.ts
type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]>; } : T;
export type HooksConfig = {
store?: StoreState<DeepPartial<ReturnType<typeof useStore>>>;
};
// TestPage.ct.ts
import { test, expect } from '@playwright/experimental-ct-vue';
import { type HooksConfig } from 'playwright';
import TestPage from './TestPage.vue';
test('display current user', async ({ mount }) => {
const component = await mount<HooksConfig>(TestPage, {
hooksConfig: {
store: {
user: { name: 'Jackson', age: 30 }
}
},
});
await expect(component.getByTestId('name')).toHaveText('Jackson');
});
I typically avoid mocking store functions anyway and rarely mock simple store properties like booleans, strings, or numbers. However, if mocking a function is really necessary, they can be defined in playwright/index.ts, which runs in the browser:
// playwright/index.ts
beforeMount<HooksConfig>(async ({ app, hooksConfig }) => {
createTestingPinia({
initialState: {
store: {
...hooksConfig?.store,
toUserString() {} // <== runs in the browser
}
},
stubActions: false,
createSpy(args) {
return () => args;
},
});
});
The solution depends on the direction the Playwright team chooses for component testing. I think they should either overhaul the current architecture perhaps to allow tests to run entirely in the browser, like Vitest’s browser mode, or improve the existing architecture with a more effective code extraction and transplantation process, along with clearer error messages for limitations, possibly supported by an ESLint plugin (similar to Qwik). However, given the limited attention Playwright is able to give component testing, due to "lack of community interest", it might take a long time—or not happen at all.
Thank you, @sand4rt, for the detailed explanation and the suggested workaround.
I understand that the Playwright component's architecture has some inherent limitations, but I didn’t expect that functions wouldn’t be simply serialized and transmitted. In my case, I’m not looking to mock functions in the store (and I generally avoid doing so), but rather to trigger a function dynamically at runtime.
As you mentioned, achieving this seems quite challenging given the current design, unless the Playwright team considers a different approach in the future.
I truly appreciate all the effort behind this incredible tool and hope it continues to gain the recognition it deserves. 👍
but I didn’t expect that functions wouldn’t be simply serialized and transmitted
Yeah, i wouldn’t consider that expected behavior either. At least some errors or warnings would definitely help here
I'm not looking to mock functions in the store (and I generally avoid doing so), but rather to trigger a function dynamically at runtime
Triggering functions manually from the test would be considered bad practice (if i understood you correctly). This approach tests implementation details rather than user behavior. Instead, it should be done by mimicking a user's actions, such as clicking or typing etc.
As you mentioned, achieving this seems quite challenging given the current design
I believe we should be fine as long as we avoid mocking or manually triggering functions, since those patterns are generally considered bad practice anyway, right?
related to; https://github.com/microsoft/playwright/issues/31361
Thank you for pointing that out and for sharing the link—I appreciate it! I realize I might have caused some confusion by using the word "trigger." You're absolutely right that manually triggering functions in a component test is considered bad practice; that approach is better suited for unit tests.
What I actually meant was testing user actions, as you described. For example, when a user clicks a button, the event handler should invoke the store function. However, if it's not possible to serialize and transmit the function to the browser env, I suppose testing such a scenario might not be feasible.
Why was this issue closed?
Thank you for your contribution to our project. This issue has been closed due to its limited upvotes and recent activity, and insufficient feedback for us to effectively act upon. Our priority is to focus on bugs that reflect higher user engagement and have actionable feedback, to ensure our bug database stays manageable.
Should you feel this closure was in error, please create a new issue and reference this one. We're open to revisiting it given increased support or additional clarity. Your understanding and cooperation are greatly appreciated.