Commit ada61b98d1 for woocommerce
commit ada61b98d16786c4dc188f6479c12e58a3a7f5a9
Author: theAverageDev (Luca Tumedei) <luca@theaveragedev.com>
Date: Fri Feb 6 11:38:27 2026 +0100
Convert e2e-utils-playwright package to TypeScript (#63102)
* Convert e2e-utils-playwright package to TypeScript
Add TypeScript configuration, type definitions, and convert source files
to use TypeScript syntax (type annotations, interfaces, generics).
diff --git a/packages/js/e2e-utils-playwright/babel.config.js b/packages/js/e2e-utils-playwright/babel.config.js
deleted file mode 100644
index 7036bf87d8..0000000000
--- a/packages/js/e2e-utils-playwright/babel.config.js
+++ /dev/null
@@ -1,11 +0,0 @@
-module.exports = {
- "presets": [
- "@wordpress/babel-preset-default",
- [
- "@babel/preset-env",
- {
- "modules": "commonjs"
- }
- ]
- ]
-};
diff --git a/packages/js/e2e-utils-playwright/changelog/e2e-utils-playwright-ts-conversion b/packages/js/e2e-utils-playwright/changelog/e2e-utils-playwright-ts-conversion
new file mode 100644
index 0000000000..ccc34f3bf1
--- /dev/null
+++ b/packages/js/e2e-utils-playwright/changelog/e2e-utils-playwright-ts-conversion
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Converted the package source code to TypeScript.
diff --git a/packages/js/e2e-utils-playwright/jest.config.json b/packages/js/e2e-utils-playwright/jest.config.json
new file mode 100644
index 0000000000..51e3213829
--- /dev/null
+++ b/packages/js/e2e-utils-playwright/jest.config.json
@@ -0,0 +1,5 @@
+{
+ "rootDir": "./",
+ "roots": [ "<rootDir>/src" ],
+ "preset": "./node_modules/@woocommerce/internal-js-tests/jest-preset.js"
+}
diff --git a/packages/js/e2e-utils-playwright/package.json b/packages/js/e2e-utils-playwright/package.json
index d0eb6af79f..865611ed07 100644
--- a/packages/js/e2e-utils-playwright/package.json
+++ b/packages/js/e2e-utils-playwright/package.json
@@ -12,6 +12,20 @@
"node": "^20.11.1"
},
"main": "build/index.js",
+ "module": "build-module/index.js",
+ "types": "build-types/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./build-types/index.d.ts",
+ "import": "./build-module/index.js",
+ "require": "./build/index.js"
+ }
+ },
+ "files": [
+ "build",
+ "build-module",
+ "build-types"
+ ],
"scripts": {
"changelog": "XDEBUG_MODE=off composer install --quiet && composer exec -- changelogger",
"update:php": "XDEBUG_MODE=off composer update --quiet",
@@ -21,21 +35,47 @@
"prepack": "pnpm build",
"build": "pnpm --if-present --workspace-concurrency=Infinity --stream --filter=\"$npm_package_name...\" '/^build:project:.*$/'",
"build:project": "pnpm --if-present '/^build:project:.*$/'",
- "build:project:esm": "wireit"
+ "build:project:cjs": "wireit",
+ "build:project:esm": "wireit",
+ "watch:build": "pnpm --if-present --workspace-concurrency=Infinity --filter=\"$npm_package_name...\" --parallel '/^watch:build:project:.*$/'",
+ "watch:build:project": "pnpm --if-present run '/^watch:build:project:.*$/'",
+ "watch:build:project:cjs": "wireit",
+ "watch:build:project:esm": "wireit"
},
"config": {
"ci": {
"lint": {
"command": "lint",
"changes": "src/**/*.{js,ts,tsx}"
- }
+ },
+ "tests": [
+ {
+ "name": "JavaScript",
+ "command": "test",
+ "changes": [
+ "jest.config.json",
+ "tsconfig.json",
+ "src/**/*.{js,jsx,ts,tsx}"
+ ],
+ "events": [
+ "pull_request",
+ "push"
+ ]
+ }
+ ]
}
},
"devDependencies": {
- "@babel/cli": "7.25.7",
- "@babel/core": "7.25.7",
- "@wordpress/babel-preset-default": "next",
+ "@playwright/test": "^1.57.0",
+ "@types/jest": "29.5.x",
+ "@types/node": "20.x.x",
+ "@woocommerce/eslint-plugin": "workspace:*",
+ "@woocommerce/internal-js-tests": "workspace:*",
+ "eslint": "^8.55.0",
"jest": "29.5.x",
+ "jest-cli": "29.5.x",
+ "ts-jest": "29.1.x",
+ "typescript": "5.7.x",
"wireit": "0.14.12"
},
"dependencies": {
@@ -46,18 +86,54 @@
"access": "public"
},
"wireit": {
- "build:project:esm": {
- "command": "babel src --out-dir build --copy-files --no-copy-ignored --ignore '**/test/**'",
+ "build:project:cjs": {
+ "command": "tsc --project tsconfig-cjs.json --noCheck",
"clean": "if-file-deleted",
"files": [
- "babel.config.js",
- "src/**/*.{js,jsx,ts,tsx}",
- "typings/**/*.ts"
+ "tsconfig-cjs.json",
+ "src/**/*.{js,jsx,ts,tsx}"
],
"output": [
"build"
],
- "dependencies": []
+ "dependencies": [
+ "dependencyOutputs"
+ ]
+ },
+ "watch:build:project:cjs": {
+ "command": "tsc --project tsconfig-cjs.json --watch --noCheck",
+ "service": true
+ },
+ "build:project:esm": {
+ "command": "tsc --project tsconfig.json",
+ "clean": "if-file-deleted",
+ "files": [
+ "tsconfig.json",
+ "src/**/*.{js,jsx,ts,tsx}"
+ ],
+ "output": [
+ "build-module",
+ "build-types"
+ ],
+ "dependencies": [
+ "dependencyOutputs"
+ ]
+ },
+ "watch:build:project:esm": {
+ "command": "tsc --project tsconfig.json --watch",
+ "service": true
+ },
+ "dependencyOutputs": {
+ "allowUsuallyExcludedPaths": true,
+ "files": [
+ "package.json",
+ "node_modules/@woocommerce/eslint-plugin/configs",
+ "node_modules/@woocommerce/eslint-plugin/rules",
+ "node_modules/@woocommerce/eslint-plugin/index.js",
+ "node_modules/@woocommerce/internal-js-tests/build",
+ "node_modules/@woocommerce/internal-js-tests/build-module",
+ "node_modules/@woocommerce/internal-js-tests/jest-preset.js"
+ ]
}
}
}
diff --git a/packages/js/e2e-utils-playwright/src/api-client.js b/packages/js/e2e-utils-playwright/src/api-client.js
deleted file mode 100644
index ab8c6e5c55..0000000000
--- a/packages/js/e2e-utils-playwright/src/api-client.js
+++ /dev/null
@@ -1,276 +0,0 @@
-/* eslint-disable jsdoc/check-property-names */
-// @ts-check
-/**
- * @typedef {Object} BasicAuth
- * @property {'basic'} type Type of authentication ('basic')
- * @property {string} username Username for basic authentication
- * @property {string} password Password for basic authentication
- *
- * @typedef {Object} OAuth1Auth
- * @property {'oauth1'} type Type of authentication ('oauth1')
- * @property {string} consumerKey OAuth1 consumer key
- * @property {string} consumerSecret OAuth1 consumer secret
- *
- * @typedef {BasicAuth|OAuth1Auth} Auth
- */
-/**
- * External dependencies
- */
-import axios from 'axios';
-import OAuth from 'oauth-1.0a';
-import { createHmac } from 'crypto';
-
-/**
- * Create an API client instance with the given configuration
- *
- * @param {string} baseURL Base URL for the API
- * @param {Object} auth Auth object: { type: 'basic', username, password } or { type: 'oauth1', consumerKey, consumerSecret }
- * @return {Object} API client instance with HTTP methods
- */
-export function createClient( baseURL, auth ) {
- if ( ! auth || typeof auth !== 'object' ) {
- throw new Error( 'auth parameter is required and must be an object' );
- }
- if ( auth.type === 'basic' ) {
- if ( ! auth.username || ! auth.password ) {
- throw new Error( 'Basic auth requires username and password' );
- }
- } else if ( auth.type === 'oauth1' ) {
- if ( ! auth.consumerKey || ! auth.consumerSecret ) {
- throw new Error(
- 'OAuth1 auth requires consumerKey and consumerSecret'
- );
- }
- } else {
- throw new Error( 'auth.type must be either "basic" or "oauth1"' );
- }
-
- // Ensure baseURL ends with '/'
- if ( ! baseURL.endsWith( '/' ) ) {
- baseURL += '/';
- }
-
- // Only append 'wp-json/' if not already present
- if ( ! baseURL.endsWith( 'wp-json/' ) ) {
- baseURL += 'wp-json/';
- }
-
- const axiosConfig = {
- baseURL,
- headers: {
- 'Content-Type': 'application/json',
- },
- };
-
- let oauth;
- if ( auth.type === 'basic' ) {
- axiosConfig.auth = {
- username: auth.username,
- password: auth.password,
- };
-
- // Warn if Basic Auth is used over HTTP, except for localhost
- const isHttp = baseURL.startsWith( 'http' );
- const isLocalhost =
- baseURL.startsWith( 'http://localhost' ) ||
- baseURL.startsWith( 'http://127.0.0.1' );
- if ( isHttp && ! isLocalhost ) {
- console.warn(
- 'Warning: Using Basic Auth over HTTP exposes credentials in plaintext!'
- );
- }
- } else if ( auth.type === 'oauth1' ) {
- oauth = new OAuth( {
- consumer: {
- key: auth.consumerKey,
- secret: auth.consumerSecret,
- },
- signature_method: 'HMAC-SHA256',
- hash_function: ( base, key ) => {
- return createHmac( 'sha256', key )
- .update( base )
- .digest( 'base64' );
- },
- } );
- }
-
- const axiosInstance = axios.create( axiosConfig );
-
- // Utility to redact sensitive fields from logs
- function redact(
- obj,
- keys = [ 'password', 'token', 'authorization', 'cookie', 'secret' ]
- ) {
- const shouldRedact = process.env.CI === 'true';
- if ( ! shouldRedact ) return obj;
- if ( ! obj || typeof obj !== 'object' ) return obj;
- return Object.fromEntries(
- Object.entries( obj ).map( ( [ k, v ] ) =>
- keys.includes( k.toLowerCase() )
- ? [ k, '********' ]
- : [ k, typeof v === 'object' ? redact( v, keys ) : v ]
- )
- );
- }
-
- // Centralized logging for requests, with redaction and formatting
- function logRequest( label, details ) {
- const redacted = Object.fromEntries(
- Object.entries( details ).map( ( [ k, v ] ) => [ k, redact( v ) ] )
- );
- console.log( `[${ new Date().toISOString() }] ${ label }`, redacted );
- }
-
- function oauthRequest(
- method,
- path,
- { params = {}, data = {}, debug = false } = {}
- ) {
- let url = baseURL + path.replace( /^\//, '' );
- let requestConfig = { method };
- let oauthParams, headers;
-
- if ( method === 'GET' ) {
- // For GET, sign the query params and append both params and OAuth params to the URL
- oauthParams = oauth.authorize( {
- url,
- method,
- data: params,
- } );
- const urlObj = new URL( url );
- Object.entries( { ...params, ...oauthParams } ).forEach(
- ( [ key, value ] ) => {
- urlObj.searchParams.append( key, value );
- }
- );
- url = urlObj.toString();
- requestConfig = { ...requestConfig, url };
- } else {
- // For POST/PUT/DELETE, sign the body if form-encoded, otherwise sign as if body is empty (for JSON)
- const isJson = (
- axiosConfig.headers[ 'Content-Type' ] || ''
- ).includes( 'application/json' );
- oauthParams = oauth.authorize( {
- url,
- method,
- data: isJson ? {} : data,
- } );
- headers = {
- ...axiosConfig.headers,
- ...oauth.toHeader( oauthParams ),
- };
- requestConfig = { ...requestConfig, url, headers, data };
- }
-
- if ( debug ) {
- logRequest( 'oauthRequest', {
- method,
- url,
- params,
- data,
- headers,
- } );
- }
- return axios( requestConfig );
- }
-
- return {
- /**
- * Make a GET request
- *
- * @param {string} path API endpoint path
- * @param {Object} params Query parameters
- * @return {Promise} Promise that resolves to response object
- */
- async get( path, params = {}, debug = false ) {
- if ( auth.type === 'oauth1' ) {
- return oauthRequest( 'GET', path, { params, debug } );
- }
- const response = await axiosInstance.get( path, { params } );
- if ( debug ) {
- logRequest( 'get', {
- path,
- params,
- status: response?.status,
- data: response?.data,
- } );
- }
- return response;
- },
-
- /**
- * Make a POST request
- *
- * @param {string} path API endpoint path
- * @param {Object} data Request body data
- * @return {Promise} Promise that resolves to response object
- */
- async post( path, data = {}, debug = false ) {
- if ( auth.type === 'oauth1' ) {
- return oauthRequest( 'POST', path, { data, debug } );
- }
- const response = await axiosInstance.post( path, data );
- if ( debug ) {
- logRequest( 'post', {
- path,
- data,
- status: response?.status,
- response: response?.data,
- } );
- }
- return response;
- },
-
- /**
- * Make a PUT request
- *
- * @param {string} path API endpoint path
- * @param {Object} data Request body data
- * @return {Promise} Promise that resolves to response object
- */
- async put( path, data = {}, debug = false ) {
- if ( auth.type === 'oauth1' ) {
- return oauthRequest( 'PUT', path, { data, debug } );
- }
- const response = await axiosInstance.put( path, data );
- if ( debug ) {
- logRequest( 'put', {
- path,
- data,
- status: response?.status,
- response: response?.data,
- } );
- }
- return response;
- },
-
- /**
- * Make a DELETE request
- *
- * @param {string} path API endpoint path
- * @param {Object} params Query parameters or request body
- * @return {Promise} Promise that resolves to response object
- */
- async delete( path, params = {}, debug = false ) {
- if ( auth.type === 'oauth1' ) {
- return oauthRequest( 'DELETE', path, { data: params, debug } );
- }
- const response = await axiosInstance.delete( path, {
- data: params,
- } );
- if ( debug ) {
- logRequest( 'delete', {
- path,
- params,
- status: response?.status,
- response: response?.data,
- } );
- }
- return response;
- },
- };
-}
-
-export const WC_API_PATH = 'wc/v3';
-export const WC_ADMIN_API_PATH = 'wc-admin';
-export const WP_API_PATH = 'wp/v2';
diff --git a/packages/js/e2e-utils-playwright/src/api-client.ts b/packages/js/e2e-utils-playwright/src/api-client.ts
new file mode 100644
index 0000000000..00e2a1b675
--- /dev/null
+++ b/packages/js/e2e-utils-playwright/src/api-client.ts
@@ -0,0 +1,351 @@
+/**
+ * External dependencies
+ */
+import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
+import OAuth from 'oauth-1.0a';
+import { createHmac } from 'crypto';
+
+/**
+ * Internal dependencies
+ */
+import type { Auth, ApiClient } from './types';
+
+// Re-export types for consumers
+export type { BasicAuth, OAuth1Auth, Auth } from './types';
+
+interface OAuthRequestOptions {
+ params?: Record< string, unknown >;
+ data?: Record< string, unknown >;
+ debug?: boolean;
+}
+
+/**
+ * Create an API client instance with the given configuration.
+ *
+ * @param baseURL - Base URL for the API
+ * @param auth - Auth object: { type: 'basic', username, password } or { type: 'oauth1', consumerKey, consumerSecret }
+ * @return API client instance with HTTP methods
+ */
+export function createClient( baseURL: string, auth: Auth ): ApiClient {
+ if ( ! auth || typeof auth !== 'object' ) {
+ throw new Error( 'auth parameter is required and must be an object' );
+ }
+ if ( auth.type === 'basic' ) {
+ if ( ! auth.username || ! auth.password ) {
+ throw new Error( 'Basic auth requires username and password' );
+ }
+ } else if ( auth.type === 'oauth1' ) {
+ if ( ! auth.consumerKey || ! auth.consumerSecret ) {
+ throw new Error(
+ 'OAuth1 auth requires consumerKey and consumerSecret'
+ );
+ }
+ } else {
+ throw new Error( 'auth.type must be either "basic" or "oauth1"' );
+ }
+
+ // Ensure baseURL ends with '/'
+ let normalizedBaseURL = baseURL;
+ if ( ! normalizedBaseURL.endsWith( '/' ) ) {
+ normalizedBaseURL += '/';
+ }
+
+ // Only append 'wp-json/' if not already present
+ if ( ! normalizedBaseURL.endsWith( 'wp-json/' ) ) {
+ normalizedBaseURL += 'wp-json/';
+ }
+
+ const axiosConfig: AxiosRequestConfig = {
+ baseURL: normalizedBaseURL,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ };
+
+ let oauth: OAuth | undefined;
+ if ( auth.type === 'basic' ) {
+ axiosConfig.auth = {
+ username: auth.username,
+ password: auth.password,
+ };
+
+ // Warn if Basic Auth is used over HTTP, except for localhost
+ const isHttp = normalizedBaseURL.startsWith( 'http://' );
+ const isLocalhost =
+ normalizedBaseURL.startsWith( 'http://localhost' ) ||
+ normalizedBaseURL.startsWith( 'http://127.0.0.1' );
+ if ( isHttp && ! isLocalhost ) {
+ console.warn(
+ 'Warning: Using Basic Auth over HTTP exposes credentials in plaintext!'
+ );
+ }
+ } else if ( auth.type === 'oauth1' ) {
+ oauth = new OAuth( {
+ consumer: {
+ key: auth.consumerKey,
+ secret: auth.consumerSecret,
+ },
+ signature_method: 'HMAC-SHA256',
+ hash_function: ( base: string, key: string ) => {
+ return createHmac( 'sha256', key )
+ .update( base )
+ .digest( 'base64' );
+ },
+ } );
+ }
+
+ const axiosInstance = axios.create( axiosConfig );
+
+ /**
+ * Utility to redact sensitive fields from logs.
+ *
+ * @param obj - Object to redact
+ * @param keys - Keys to redact
+ * @return Redacted object
+ */
+ function redact(
+ obj: Record< string, unknown > | null | undefined,
+ keys: string[] = [
+ 'password',
+ 'token',
+ 'authorization',
+ 'cookie',
+ 'secret',
+ ]
+ ): Record< string, unknown > | null | undefined {
+ const shouldRedact = process.env.CI === 'true';
+ if ( ! shouldRedact ) return obj;
+ if ( ! obj || typeof obj !== 'object' ) return obj;
+ return Object.fromEntries(
+ Object.entries( obj ).map( ( [ k, v ] ) =>
+ keys.includes( k.toLowerCase() )
+ ? [ k, '********' ]
+ : [
+ k,
+ typeof v === 'object'
+ ? redact( v as Record< string, unknown >, keys )
+ : v,
+ ]
+ )
+ );
+ }
+
+ /**
+ * Centralized logging for requests, with redaction and formatting.
+ *
+ * @param label - Log label
+ * @param details - Details to log
+ */
+ function logRequest( label: string, details: Record< string, unknown > ) {
+ const redacted = Object.fromEntries(
+ Object.entries( details ).map( ( [ k, v ] ) => [
+ k,
+ redact( v as Record< string, unknown > ),
+ ] )
+ );
+ console.log( `[${ new Date().toISOString() }] ${ label }`, redacted );
+ }
+
+ /**
+ * Make an OAuth-authenticated request.
+ *
+ * @param method - HTTP method
+ * @param path - API endpoint path
+ * @param options - Request options
+ * @param options.params - Query parameters
+ * @param options.data - Request body data
+ * @param options.debug - Enable debug logging
+ * @return Promise resolving to the response
+ */
+ function oauthRequest(
+ method: string,
+ path: string,
+ { params = {}, data = {}, debug = false }: OAuthRequestOptions = {}
+ ): Promise< AxiosResponse > {
+ if ( ! oauth ) {
+ throw new Error( 'OAuth not initialized' );
+ }
+
+ let url = normalizedBaseURL + path.replace( /^\//, '' );
+ let requestConfig: AxiosRequestConfig = { method };
+ let oauthParams: OAuth.Authorization;
+ let headers: Record< string, string > | undefined;
+
+ if ( method === 'GET' ) {
+ // For GET, sign the query params and append both params and OAuth params to the URL
+ oauthParams = oauth.authorize( {
+ url,
+ method,
+ data: params,
+ } );
+ const urlObj = new URL( url );
+ Object.entries( { ...params, ...oauthParams } ).forEach(
+ ( [ key, value ] ) => {
+ urlObj.searchParams.append( key, String( value ) );
+ }
+ );
+ url = urlObj.toString();
+ requestConfig = { ...requestConfig, url };
+ } else {
+ // For POST/PUT/DELETE, sign the body if form-encoded, otherwise sign as if body is empty (for JSON)
+ const contentType =
+ (
+ axiosConfig.headers as Record< string, string > | undefined
+ )?.[ 'Content-Type' ] || '';
+ const isJson = contentType.includes( 'application/json' );
+ oauthParams = oauth.authorize( {
+ url,
+ method,
+ data: isJson ? {} : data,
+ } );
+ headers = {
+ ...( axiosConfig.headers as Record< string, string > ),
+ ...oauth.toHeader( oauthParams ),
+ };
+ requestConfig = { ...requestConfig, url, headers, data };
+ }
+
+ if ( debug ) {
+ logRequest( 'oauthRequest', {
+ method,
+ url,
+ params,
+ data,
+ headers,
+ } );
+ }
+ return axios( requestConfig );
+ }
+
+ return {
+ /**
+ * Make a GET request.
+ *
+ * @param path - API endpoint path
+ * @param params - Query parameters
+ * @param debug - Enable debug logging
+ * @return Promise that resolves to response object
+ */
+ async get< T = unknown >(
+ path: string,
+ params: Record< string, unknown > = {},
+ debug = false
+ ): Promise< AxiosResponse< T > > {
+ if ( auth.type === 'oauth1' ) {
+ return oauthRequest( 'GET', path, {
+ params,
+ debug,
+ } );
+ }
+ const response = await axiosInstance.get< T >( path, { params } );
+ if ( debug ) {
+ logRequest( 'get', {
+ path,
+ params,
+ status: response?.status,
+ data: response?.data,
+ } );
+ }
+ return response;
+ },
+
+ /**
+ * Make a POST request.
+ *
+ * @param path - API endpoint path
+ * @param data - Request body data
+ * @param debug - Enable debug logging
+ * @return Promise that resolves to response object
+ */
+ async post< T = unknown >(
+ path: string,
+ data: Record< string, unknown > = {},
+ debug = false
+ ): Promise< AxiosResponse< T > > {
+ if ( auth.type === 'oauth1' ) {
+ return oauthRequest( 'POST', path, {
+ data,
+ debug,
+ } );
+ }
+ const response = await axiosInstance.post< T >( path, data );
+ if ( debug ) {
+ logRequest( 'post', {
+ path,
+ data,
+ status: response?.status,
+ response: response?.data,
+ } );
+ }
+ return response;
+ },
+
+ /**
+ * Make a PUT request.
+ *
+ * @param path - API endpoint path
+ * @param data - Request body data
+ * @param debug - Enable debug logging
+ * @return Promise that resolves to response object
+ */
+ async put< T = unknown >(
+ path: string,
+ data: Record< string, unknown > = {},
+ debug = false
+ ): Promise< AxiosResponse< T > > {
+ if ( auth.type === 'oauth1' ) {
+ return oauthRequest( 'PUT', path, {
+ data,
+ debug,
+ } );
+ }
+ const response = await axiosInstance.put< T >( path, data );
+ if ( debug ) {
+ logRequest( 'put', {
+ path,
+ data,
+ status: response?.status,
+ response: response?.data,
+ } );
+ }
+ return response;
+ },
+
+ /**
+ * Make a DELETE request.
+ *
+ * @param path - API endpoint path
+ * @param params - Query parameters or request body
+ * @param debug - Enable debug logging
+ * @return Promise that resolves to response object
+ */
+ async delete< T = unknown >(
+ path: string,
+ params: Record< string, unknown > = {},
+ debug = false
+ ): Promise< AxiosResponse< T > > {
+ if ( auth.type === 'oauth1' ) {
+ return oauthRequest( 'DELETE', path, {
+ data: params,
+ debug,
+ } );
+ }
+ const response = await axiosInstance.delete< T >( path, {
+ data: params,
+ } );
+ if ( debug ) {
+ logRequest( 'delete', {
+ path,
+ params,
+ status: response?.status,
+ response: response?.data,
+ } );
+ }
+ return response;
+ },
+ };
+}
+
+export const WC_API_PATH = 'wc/v3';
+export const WC_ADMIN_API_PATH = 'wc-admin';
+export const WP_API_PATH = 'wp/v2';
diff --git a/packages/js/e2e-utils-playwright/src/cart.js b/packages/js/e2e-utils-playwright/src/cart.ts
similarity index 52%
rename from packages/js/e2e-utils-playwright/src/cart.js
rename to packages/js/e2e-utils-playwright/src/cart.ts
index f9e1d4a8e8..63551930d1 100644
--- a/packages/js/e2e-utils-playwright/src/cart.js
+++ b/packages/js/e2e-utils-playwright/src/cart.ts
@@ -1,11 +1,20 @@
+/**
+ * External dependencies
+ */
+import type { Page } from '@playwright/test';
+
/**
* Adds a specified quantity of a product by ID to the WooCommerce cart.
*
- * @param {import('playwright').Page} page
- * @param {string} productId
- * @param {number} [quantity=1]
+ * @param page - Playwright page object
+ * @param productId - The product ID to add
+ * @param quantity - Number of items to add (default: 1)
*/
-export const addAProductToCart = async ( page, productId, quantity = 1 ) => {
+export const addAProductToCart = async (
+ page: Page,
+ productId: string,
+ quantity = 1
+): Promise< void > => {
for ( let i = 0; i < quantity; i++ ) {
const responsePromise = page.waitForResponse(
'**/wp-json/wc/store/v1/cart**'
@@ -17,17 +26,17 @@ export const addAProductToCart = async ( page, productId, quantity = 1 ) => {
};
/**
- * Util helper made for adding multiple same products to cart
+ * Util helper made for adding multiple same products to cart.
*
- * @param {import('playwright').Page} page
- * @param {string} productName
- * @param {number} quantityCount
+ * @param page - Playwright page object
+ * @param productName - Name of the product to add
+ * @param quantityCount - Number of items to add (default: 1)
*/
export async function addOneOrMoreProductToCart(
- page,
- productName,
+ page: Page,
+ productName: string,
quantityCount = 1
-) {
+): Promise< void > {
await page.goto(
`product/${ productName.replace( / /gi, '-' ).toLowerCase() }`
);
diff --git a/packages/js/e2e-utils-playwright/src/checkout.js b/packages/js/e2e-utils-playwright/src/checkout.js
deleted file mode 100644
index 19dbc02b1c..0000000000
--- a/packages/js/e2e-utils-playwright/src/checkout.js
+++ /dev/null
@@ -1,207 +0,0 @@
-/**
- * Util helper made to fill the Checkout details in the block-based checkout.
- *
- * @param {Object} page
- * @param {Object} [details={}] - The shipping details object.
- * @param {string} [details.country='US'] - The first name.
- * @param {string} [details.firstName='Mister'] - The first name.
- * @param {string} [details.lastName='Burns'] - The last name.
- * @param {string} [details.address='156th Street'] - The address.
- * @param {string} [details.zip=''] - The ZIP code.
- * @param {string} [details.city=''] - The city.
- * @param {string} [details.state=''] - The State.
- * @param {string} [details.suburb=''] - The Suburb.
- * @param {string} [details.province=''] - The Province.
- * @param {string} [details.district=''] - The District.
- * @param {string} [details.department=''] - The Department.
- * @param {string} [details.region=''] - The Region.
- * @param {string} [details.parish=''] - The Parish.
- * @param {string} [details.county=''] - The Country.
- * @param {string} [details.prefecture=''] - The Prefecture.
- * @param {string} [details.municipality=''] - The Municipality.
- * @param {boolean} [details.isPostalCode=false] - If true, search by 'Postal code' instead of 'Zip Code'.
- */
-async function fillCheckoutBlocks( page, details = {}, type = 'shipping' ) {
- const {
- country = '',
- firstName = '',
- lastName = '',
- address = '',
- zip = '',
- city = '',
- state = '',
- suburb = '',
- province = '',
- district = '',
- department = '',
- region = '',
- parish = '',
- county = '',
- prefecture = '',
- municipality = '',
- phone = '',
- isPostalCode = false,
- } = details;
-
- const label = {
- shipping: 'Shipping address',
- billing: 'Billing address',
- };
-
- async function setDynamicFieldType( field, addressElement ) {
- const tagName = await field.evaluate( ( el ) =>
- el.tagName.toLowerCase()
- );
-
- if ( tagName === 'select' ) {
- await field.selectOption( addressElement );
- } else {
- await field.fill( addressElement );
- }
- }
-
- await page
- .getByRole( 'group', { name: label[ type ] } )
- .getByLabel( 'First name' )
- .fill( firstName );
-
- await page
- .getByRole( 'group', { name: label[ type ] } )
- .getByLabel( 'Last name' )
- .fill( lastName );
-
- await page
- .getByRole( 'group', { name: label[ type ] } )
- .getByLabel( 'Address', { exact: true } )
- .fill( address );
-
- if ( country ) {
- await page
- .getByRole( 'group', { name: label[ type ] } )
- .getByLabel( 'Country' )
- .selectOption( country );
- }
-
- if ( city ) {
- await page
- .getByRole( 'group', { name: label[ type ] } )
- .getByLabel( 'City' )
- .fill( city );
- }
-
- if ( suburb ) {
- await page
- .getByRole( 'group', { name: label[ type ] } )
- .getByLabel( 'Suburb' )
- .fill( suburb );
- }
-
- if ( province ) {
- await page
- .getByRole( 'group', { name: label[ type ] } )
- .getByLabel( 'Province' )
- .selectOption( province );
- }
-
- if ( district ) {
- await page
- .getByRole( 'group', { name: label[ type ] } )
- .getByLabel( 'District' )
- .selectOption( district );
- }
-
- if ( department ) {
- await setDynamicFieldType(
- await page
- .getByRole( 'group', { name: label[ type ] } )
- .getByLabel( 'Department' ),
- department
- );
- }
-
- if ( region ) {
- await page
- .getByRole( 'group', { name: label[ type ] } )
- .getByLabel( 'Region', { exact: true } )
- .selectOption( region );
- }
-
- if ( parish ) {
- await setDynamicFieldType(
- await page
- .getByRole( 'group', { name: label[ type ] } )
- .getByLabel( 'Parish', { exact: false } ),
- parish
- );
- }
-
- if ( county ) {
- await setDynamicFieldType(
- await page
- .getByRole( 'group', { name: label[ type ] } )
- .getByLabel( 'County' ),
- county
- );
- }
-
- if ( prefecture ) {
- await page
- .getByRole( 'group', { name: label[ type ] } )
- .getByLabel( 'Prefecture' )
- .selectOption( prefecture );
- }
-
- if ( municipality ) {
- await page
- .getByRole( 'group', { name: label[ type ] } )
- .getByLabel( 'Municipality' )
- .fill( municipality );
- }
-
- if ( state ) {
- const stateField = await page
- .getByRole( 'group', { name: label[ type ] } )
- .getByLabel( 'State/County', { exact: false } )
- .or(
- await page
- .getByRole( 'group', { name: label[ type ] } )
- .getByLabel( 'State' )
- );
-
- await setDynamicFieldType( stateField, state );
- }
-
- if ( zip ) {
- await page
- .getByRole( 'group', { name: label[ type ] } )
- .getByLabel( isPostalCode ? 'Postal code' : 'ZIP Code' )
- .fill( zip );
- }
-
- if ( phone ) {
- await page
- .getByRole( 'group', { name: label[ type ] } )
- .getByRole( 'textbox', { name: 'Phone' } )
- .fill( phone );
- }
-}
-
-/**
- * Convenience function to fill Shipping Address fields.
- *
- * @param {Object} page
- * @param {*} shippingDetails See arguments description for `fillCheckoutBlocks`.
- */
-export async function fillShippingCheckoutBlocks( page, shippingDetails = {} ) {
- await fillCheckoutBlocks( page, shippingDetails, 'shipping' );
-}
-
-/**
- * Convenience function to fill Billing Address fields.
- *
- * @param {Object} page
- * @param {*} billingDetails See arguments description for `fillCheckoutBlocks`.
- */
-export async function fillBillingCheckoutBlocks( page, billingDetails = {} ) {
- await fillCheckoutBlocks( page, billingDetails, 'billing' );
-}
diff --git a/packages/js/e2e-utils-playwright/src/checkout.ts b/packages/js/e2e-utils-playwright/src/checkout.ts
new file mode 100644
index 0000000000..87ea4b791e
--- /dev/null
+++ b/packages/js/e2e-utils-playwright/src/checkout.ts
@@ -0,0 +1,216 @@
+/**
+ * External dependencies
+ */
+import type { Page, Locator } from '@playwright/test';
+
+/**
+ * Internal dependencies
+ */
+import type { CheckoutDetails, AddressType } from './types';
+
+// Re-export types for consumers
+export type { CheckoutDetails } from './types';
+
+const addressLabels: Record< AddressType, string > = {
+ shipping: 'Shipping address',
+ billing: 'Billing address',
+};
+
+/**
+ * Sets a field value based on its element type (select or input).
+ *
+ * @param field - The field locator
+ * @param value - The value to set
+ */
+async function setDynamicFieldType( field: Locator, value: string ) {
+ const tagName = await field.evaluate( ( el ) => el.tagName.toLowerCase() );
+
+ if ( tagName === 'select' ) {
+ await field.selectOption( value );
+ } else {
+ await field.fill( value );
+ }
+}
+
+/**
+ * Util helper made to fill the Checkout details in the block-based checkout.
+ *
+ * @param page - Playwright page object
+ * @param details - The checkout details object
+ * @param type - The address type ('shipping' or 'billing')
+ */
+async function fillCheckoutBlocks(
+ page: Page,
+ details: CheckoutDetails = {},
+ type: AddressType = 'shipping'
+) {
+ const {
+ country = '',
+ firstName = '',
+ lastName = '',
+ address = '',
+ zip = '',
+ city = '',
+ state = '',
+ suburb = '',
+ province = '',
+ district = '',
+ department = '',
+ region = '',
+ parish = '',
+ county = '',
+ prefecture = '',
+ municipality = '',
+ phone = '',
+ isPostalCode = false,
+ } = details;
+
+ const label = addressLabels[ type ];
+
+ await page
+ .getByRole( 'group', { name: label } )
+ .getByLabel( 'First name' )
+ .fill( firstName );
+
+ await page
+ .getByRole( 'group', { name: label } )
+ .getByLabel( 'Last name' )
+ .fill( lastName );
+
+ await page
+ .getByRole( 'group', { name: label } )
+ .getByLabel( 'Address', { exact: true } )
+ .fill( address );
+
+ if ( country ) {
+ await page
+ .getByRole( 'group', { name: label } )
+ .getByLabel( 'Country' )
+ .selectOption( country );
+ }
+
+ if ( city ) {
+ await page
+ .getByRole( 'group', { name: label } )
+ .getByLabel( 'City' )
+ .fill( city );
+ }
+
+ if ( suburb ) {
+ await page
+ .getByRole( 'group', { name: label } )
+ .getByLabel( 'Suburb' )
+ .fill( suburb );
+ }
+
+ if ( province ) {
+ await page
+ .getByRole( 'group', { name: label } )
+ .getByLabel( 'Province' )
+ .selectOption( province );
+ }
+
+ if ( district ) {
+ await page
+ .getByRole( 'group', { name: label } )
+ .getByLabel( 'District' )
+ .selectOption( district );
+ }
+
+ if ( department ) {
+ await setDynamicFieldType(
+ page
+ .getByRole( 'group', { name: label } )
+ .getByLabel( 'Department' ),
+ department
+ );
+ }
+
+ if ( region ) {
+ await page
+ .getByRole( 'group', { name: label } )
+ .getByLabel( 'Region', { exact: true } )
+ .selectOption( region );
+ }
+
+ if ( parish ) {
+ await setDynamicFieldType(
+ page
+ .getByRole( 'group', { name: label } )
+ .getByLabel( 'Parish', { exact: false } ),
+ parish
+ );
+ }
+
+ if ( county ) {
+ await setDynamicFieldType(
+ page.getByRole( 'group', { name: label } ).getByLabel( 'County' ),
+ county
+ );
+ }
+
+ if ( prefecture ) {
+ await page
+ .getByRole( 'group', { name: label } )
+ .getByLabel( 'Prefecture' )
+ .selectOption( prefecture );
+ }
+
+ if ( municipality ) {
+ await page
+ .getByRole( 'group', { name: label } )
+ .getByLabel( 'Municipality' )
+ .fill( municipality );
+ }
+
+ if ( state ) {
+ const stateField = page
+ .getByRole( 'group', { name: label } )
+ .getByLabel( 'State/County', { exact: false } )
+ .or(
+ page.getByRole( 'group', { name: label } ).getByLabel( 'State' )
+ );
+
+ await setDynamicFieldType( stateField, state );
+ }
+
+ if ( zip ) {
+ await page
+ .getByRole( 'group', { name: label } )
+ .getByLabel( isPostalCode ? 'Postal code' : 'ZIP Code' )
+ .fill( zip );
+ }
+
+ if ( phone ) {
+ await page
+ .getByRole( 'group', { name: label } )
+ .getByRole( 'textbox', { name: 'Phone' } )
+ .fill( phone );
+ }
+}
+
+/**
+ * Convenience function to fill Shipping Address fields.
+ *
+ * @param page - Playwright page object
+ * @param shippingDetails - See CheckoutDetails type for available fields
+ */
+export async function fillShippingCheckoutBlocks(
+ page: Page,
+ shippingDetails: CheckoutDetails = {}
+): Promise< void > {
+ await fillCheckoutBlocks( page, shippingDetails, 'shipping' );
+}
+
+/**
+ * Convenience function to fill Billing Address fields.
+ *
+ * @param page - Playwright page object
+ * @param billingDetails - See CheckoutDetails type for available fields
+ */
+export async function fillBillingCheckoutBlocks(
+ page: Page,
+ billingDetails: CheckoutDetails = {}
+): Promise< void > {
+ await fillCheckoutBlocks( page, billingDetails, 'billing' );
+}
diff --git a/packages/js/e2e-utils-playwright/src/editor.js b/packages/js/e2e-utils-playwright/src/editor.js
deleted file mode 100644
index e3cf88d0aa..0000000000
--- a/packages/js/e2e-utils-playwright/src/editor.js
+++ /dev/null
@@ -1,142 +0,0 @@
-export const closeChoosePatternModal = async ( { page } ) => {
- const closeModal = page
- .locator( 'div' )
- .filter( { hasText: 'Choose a pattern' } )
- .getByLabel( 'Close' );
- await page.addLocatorHandler( closeModal, async () => {
- await closeModal.click();
- } );
-};
-
-export const disableWelcomeModal = async ( { page } ) => {
- // Further info: https://github.com/woocommerce/woocommerce/pull/45856/
- await page.waitForLoadState( 'domcontentloaded' );
-
- const isWelcomeGuideActive = await page.evaluate( () =>
- window.wp.data
- .select( 'core/edit-post' )
- .isFeatureActive( 'welcomeGuide' )
- );
-
- if ( isWelcomeGuideActive ) {
- await page.evaluate( () =>
- window.wp.data
- .dispatch( 'core/edit-post' )
- .toggleFeature( 'welcomeGuide' )
- );
- }
-};
-
-export const openEditorSettings = async ( { page } ) => {
- // Open Settings sidebar if closed
- if ( await page.getByLabel( 'Editor Settings' ).isVisible() ) {
- console.log( 'Editor Settings is open, skipping action.' );
- } else {
- await page.getByLabel( 'Settings', { exact: true } ).click();
- }
-};
-
-/**
- * Returns the editor canvas frame for Gutenberg interactions.
- *
- * The Gutenberg editor content can be contained within an iframe in some contexts.
- * This helper function returns the content frame of the editor canvas iframe if it exists,
- * or falls back to the main page if the iframe isn't present.
- *
- * @param {import('@playwright/test').Page} page - The Playwright page object
- * @return {Promise<import('@playwright/test').FrameLocator|import('@playwright/test').Page>} The editor canvas frame or the original page
- */
-export const getCanvas = async ( page ) => {
- return (
- page.locator( 'iframe[name="editor-canvas"]' ).contentFrame() || page
- );
-};
-
-export const goToPageEditor = async ( { page } ) => {
- await page.goto( 'wp-admin/post-new.php?post_type=page' );
- await disableWelcomeModal( { page } );
- await closeChoosePatternModal( { page } );
-};
-
-export const goToPostEditor = async ( { page } ) => {
- await page.goto( 'wp-admin/post-new.php' );
- await disableWelcomeModal( { page } );
-};
-
-export const insertBlock = async ( page, blockName ) => {
- // Focus on "Empty block" element before inserting a new block.
- // Otherwise, Gutenberg nightly (v19.9-nightly) would display "{Block name} can't be inserted."
- const emptyBlock = ( await getCanvas( page ) ).getByLabel( 'Empty block' );
- if ( await emptyBlock.isVisible() ) {
- await emptyBlock.click();
- }
-
- // With Gutenberg active we have Block Inserter name
- await page
- .getByRole( 'button', {
- name: /Toggle block inserter|Block Inserter/,
- expanded: false,
- } )
- .click();
-
- await page.getByPlaceholder( 'Search', { exact: true } ).fill( blockName );
- await page.getByRole( 'option', { name: blockName, exact: true } ).click();
-
- await page
- .getByRole( 'button', {
- name: 'Close block inserter',
- } )
- .click();
-};
-
-export const insertBlockByShortcut = async ( page, blockName ) => {
- const canvas = await getCanvas( page );
- const emptyBlockField = canvas.getByText( 'Type / to choose a block' ).or(
- canvas.getByRole( 'document', {
- name: 'Empty block; start writing or type forward slash to choose a block',
- } )
- );
- await emptyBlockField.click();
- await emptyBlockField.pressSequentially( `/${ blockName }` );
- await page.getByRole( 'option', { name: blockName, exact: true } ).click();
-};
-
-export const transformIntoBlocks = async ( page ) => {
- const canvas = await getCanvas( page );
-
- await canvas
- .getByRole( 'button' )
- .filter( { hasText: 'Transform into blocks' } )
- .click();
-};
-
-export const publishPage = async ( page, pageTitle, isPost = false ) => {
- await page
- .getByRole( 'button', { name: 'Publish', exact: true } )
- .dispatchEvent( 'click' );
-
- const createPageResponse = page.waitForResponse( ( response ) => {
- return (
- response.url().includes( isPost ? '/posts/' : '/pages/' ) &&
- response.ok() &&
- response.request().method() === 'POST' &&
- response
- .json()
- .then(
- ( json ) =>
- json.title.rendered === pageTitle &&
- json.status === 'publish'
- )
- );
- } );
-
- await page
- .getByRole( 'region', { name: 'Editor publish' } )
- .getByRole( 'button', { name: 'Publish', exact: true } )
- .click();
-
- // Validating that page was published via UI elements is not reliable,
- // installed plugins (e.g. WooCommerce PayPal Payments) can interfere and add flakiness to the flow.
- // In WC context, checking the API response is possibly the most reliable way to ensure the page was published.
- await createPageResponse;
-};
diff --git a/packages/js/e2e-utils-playwright/src/editor.ts b/packages/js/e2e-utils-playwright/src/editor.ts
new file mode 100644
index 0000000000..e970b9f2d8
--- /dev/null
+++ b/packages/js/e2e-utils-playwright/src/editor.ts
@@ -0,0 +1,256 @@
+/**
+ * External dependencies
+ */
+import type { Page } from '@playwright/test';
+
+/**
+ * Internal dependencies
+ */
+import type { PageContext, EditorCanvas } from './types';
+
+// Re-export types for consumers
+export type { PageContext, EditorCanvas } from './types';
+
+/**
+ * WordPress data store interface for type checking.
+ */
+interface WpData {
+ select: ( store: string ) => {
+ isFeatureActive: ( feature: string ) => boolean;
+ };
+ dispatch: ( store: string ) => {
+ toggleFeature: ( feature: string ) => void;
+ };
+}
+
+type WindowWithWp = Window & { wp?: { data?: WpData } };
+
+/**
+ * Closes the "Choose a pattern" modal if present.
+ *
+ * @param context - Object containing the Playwright page
+ * @param context.page - The Playwright page object
+ */
+export const closeChoosePatternModal = async ( {
+ page,
+}: PageContext ): Promise< void > => {
+ const closeModal = page
+ .locator( 'div' )
+ .filter( { hasText: 'Choose a pattern' } )
+ .getByLabel( 'Close' );
+ await page.addLocatorHandler( closeModal, async () => {
+ await closeModal.click();
+ } );
+};
+
+/**
+ * Disables the Gutenberg welcome modal.
+ *
+ * @param context - Object containing the Playwright page
+ * @param context.page - The Playwright page object
+ */
+export const disableWelcomeModal = async ( {
+ page,
+}: PageContext ): Promise< void > => {
+ // Further info: https://github.com/woocommerce/woocommerce/pull/45856/
+ await page.waitForLoadState( 'domcontentloaded' );
+
+ const isWelcomeGuideActive = await page.evaluate( () =>
+ ( window as unknown as WindowWithWp ).wp?.data
+ ?.select( 'core/edit-post' )
+ ?.isFeatureActive( 'welcomeGuide' )
+ );
+
+ if ( isWelcomeGuideActive ) {
+ await page.evaluate( () =>
+ ( window as unknown as WindowWithWp ).wp?.data
+ ?.dispatch( 'core/edit-post' )
+ ?.toggleFeature( 'welcomeGuide' )
+ );
+ }
+};
+
+/**
+ * Opens the editor settings sidebar if closed.
+ *
+ * @param context - Object containing the Playwright page
+ * @param context.page - The Playwright page object
+ */
+export const openEditorSettings = async ( {
+ page,
+}: PageContext ): Promise< void > => {
+ // Open Settings sidebar if closed
+ if ( await page.getByLabel( 'Editor Settings' ).isVisible() ) {
+ console.log( 'Editor Settings is open, skipping action.' );
+ } else {
+ await page.getByLabel( 'Settings', { exact: true } ).click();
+ }
+};
+
+/**
+ * Returns the editor canvas frame for Gutenberg interactions.
+ *
+ * The Gutenberg editor content can be contained within an iframe in some contexts.
+ * This helper function returns the content frame of the editor canvas iframe if it exists,
+ * or falls back to the main page if the iframe isn't present.
+ *
+ * @param page - The Playwright page object
+ * @return The editor canvas frame or the original page
+ */
+export const getCanvas = async ( page: Page ): Promise< EditorCanvas > => {
+ const iframeLocator = page.locator( 'iframe[name="editor-canvas"]' );
+ if ( ( await iframeLocator.count() ) > 0 ) {
+ return iframeLocator.contentFrame();
+ }
+ return page;
+};
+
+/**
+ * Navigates to the WordPress page editor.
+ *
+ * @param context - Object containing the Playwright page
+ * @param context.page - The Playwright page object
+ */
+export const goToPageEditor = async ( {
+ page,
+}: PageContext ): Promise< void > => {
+ await page.goto( 'wp-admin/post-new.php?post_type=page' );
+ await disableWelcomeModal( { page } );
+ await closeChoosePatternModal( { page } );
+};
+
+/**
+ * Navigates to the WordPress post editor.
+ *
+ * @param context - Object containing the Playwright page
+ * @param context.page - The Playwright page object
+ */
+export const goToPostEditor = async ( {
+ page,
+}: PageContext ): Promise< void > => {
+ await page.goto( 'wp-admin/post-new.php' );
+ await disableWelcomeModal( { page } );
+};
+
+/**
+ * Inserts a block using the block inserter.
+ *
+ * @param page - The Playwright page object
+ * @param blockName - The name of the block to insert
+ */
+export const insertBlock = async (
+ page: Page,
+ blockName: string
+): Promise< void > => {
+ // Focus on "Empty block" element before inserting a new block.
+ // Otherwise, Gutenberg nightly (v19.9-nightly) would display "{Block name} can't be inserted."
+ const canvas = await getCanvas( page );
+ const emptyBlock = canvas.getByLabel( 'Empty block' );
+ if ( await emptyBlock.isVisible() ) {
+ await emptyBlock.click();
+ }
+
+ // With Gutenberg active we have Block Inserter name
+ await page
+ .getByRole( 'button', {
+ name: /Toggle block inserter|Block Inserter/,
+ expanded: false,
+ } )
+ .click();
+
+ await page.getByPlaceholder( 'Search', { exact: true } ).fill( blockName );
+ await page.getByRole( 'option', { name: blockName, exact: true } ).click();
+
+ await page
+ .getByRole( 'button', {
+ name: 'Close block inserter',
+ } )
+ .click();
+};
+
+/**
+ * Inserts a block using the slash command shortcut.
+ *
+ * @param page - The Playwright page object
+ * @param blockName - The name of the block to insert
+ */
+export const insertBlockByShortcut = async (
+ page: Page,
+ blockName: string
+): Promise< void > => {
+ const canvas = await getCanvas( page );
+ const emptyBlockField = canvas.getByText( 'Type / to choose a block' ).or(
+ canvas.getByRole( 'document', {
+ name: 'Empty block; start writing or type forward slash to choose a block',
+ } )
+ );
+ await emptyBlockField.click();
+ await emptyBlockField.pressSequentially( `/${ blockName }` );
+ await page.getByRole( 'option', { name: blockName, exact: true } ).click();
+};
+
+/**
+ * Transforms classic content into blocks.
+ *
+ * @param page - The Playwright page object
+ */
+export const transformIntoBlocks = async ( page: Page ): Promise< void > => {
+ const canvas = await getCanvas( page );
+
+ await canvas
+ .getByRole( 'button' )
+ .filter( { hasText: 'Transform into blocks' } )
+ .click();
+};
+
+/**
+ * Response JSON structure for published pages/posts.
+ */
+interface PublishResponse {
+ title: {
+ rendered: string;
+ };
+ status: string;
+}
+
+/**
+ * Publishes a page or post.
+ *
+ * @param page - The Playwright page object
+ * @param pageTitle - The title of the page/post being published
+ * @param isPost - Whether this is a post (true) or page (false)
+ */
+export const publishPage = async (
+ page: Page,
+ pageTitle: string,
+ isPost = false
+): Promise< void > => {
+ await page
+ .getByRole( 'button', { name: 'Publish', exact: true } )
+ .dispatchEvent( 'click' );
+
+ const createPageResponse = page.waitForResponse( ( response ) => {
+ return (
+ response.url().includes( isPost ? '/posts/' : '/pages/' ) &&
+ response.ok() &&
+ response.request().method() === 'POST' &&
+ response
+ .json()
+ .then(
+ ( json: PublishResponse ) =>
+ json.title.rendered === pageTitle &&
+ json.status === 'publish'
+ )
+ );
+ } );
+
+ await page
+ .getByRole( 'region', { name: 'Editor publish' } )
+ .getByRole( 'button', { name: 'Publish', exact: true } )
+ .click();
+
+ // Validating that page was published via UI elements is not reliable,
+ // installed plugins (e.g. WooCommerce PayPal Payments) can interfere and add flakiness to the flow.
+ // In WC context, checking the API response is possibly the most reliable way to ensure the page was published.
+ await createPageResponse;
+};
diff --git a/packages/js/e2e-utils-playwright/src/index.js b/packages/js/e2e-utils-playwright/src/index.js
deleted file mode 100644
index 1e258355e8..0000000000
--- a/packages/js/e2e-utils-playwright/src/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export * from './cart';
-export * from './checkout';
-export * from './editor';
-export * from './order';
-export * from './api-client';
diff --git a/packages/js/e2e-utils-playwright/src/index.ts b/packages/js/e2e-utils-playwright/src/index.ts
new file mode 100644
index 0000000000..c14224dd58
--- /dev/null
+++ b/packages/js/e2e-utils-playwright/src/index.ts
@@ -0,0 +1,9 @@
+// Re-export all modules
+export * from './cart';
+export * from './checkout';
+export * from './editor';
+export * from './order';
+export * from './api-client';
+
+// Re-export types not already covered by module re-exports above
+export type { ApiClient, AddressType } from './types';
diff --git a/packages/js/e2e-utils-playwright/src/order.js b/packages/js/e2e-utils-playwright/src/order.js
deleted file mode 100644
index 9a99f48791..0000000000
--- a/packages/js/e2e-utils-playwright/src/order.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export function getOrderIdFromUrl( page ) {
- const regex = /order-received\/(\d+)/;
- try {
- return page.url().match( regex )[ 1 ];
- } catch ( error ) {
- return undefined;
- }
-}
diff --git a/packages/js/e2e-utils-playwright/src/order.ts b/packages/js/e2e-utils-playwright/src/order.ts
new file mode 100644
index 0000000000..4153e0448f
--- /dev/null
+++ b/packages/js/e2e-utils-playwright/src/order.ts
@@ -0,0 +1,16 @@
+/**
+ * External dependencies
+ */
+import type { Page } from '@playwright/test';
+
+/**
+ * Extracts the order ID from the current page URL.
+ *
+ * @param page - Playwright page object
+ * @return The order ID or undefined if not found
+ */
+export function getOrderIdFromUrl( page: Page ): string | undefined {
+ const regex = /order-received\/(\d+)/;
+ const match = page.url().match( regex );
+ return match?.[ 1 ];
+}
diff --git a/packages/js/e2e-utils-playwright/src/test/order.test.js b/packages/js/e2e-utils-playwright/src/test/order.test.js
deleted file mode 100644
index 57423bdf8b..0000000000
--- a/packages/js/e2e-utils-playwright/src/test/order.test.js
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Internal dependencies
- */
-import { getOrderIdFromUrl } from '../order';
-
-describe( 'getOrderIdFromUrl', () => {
- it( 'should extract the order ID from a valid URL', () => {
- const page = { url: () => 'https://example.com/order-received/12345/' };
- const orderId = getOrderIdFromUrl( page );
- expect( orderId ).toBe( '12345' );
- } );
-
- it( 'should return undefined if the URL does not contain an order ID', () => {
- const page = { url: () => 'https://example.com/order-received/' };
- const orderId = getOrderIdFromUrl( page );
- expect( orderId ).toBeUndefined();
- } );
-
- it( 'should return undefined if the URL is not in the expected format', () => {
- const page = { url: () => 'https://example.com/other-page/12345/' };
- const orderId = getOrderIdFromUrl( page );
- expect( orderId ).toBeUndefined();
- } );
-} );
diff --git a/packages/js/e2e-utils-playwright/src/test/order.test.ts b/packages/js/e2e-utils-playwright/src/test/order.test.ts
new file mode 100644
index 0000000000..0a11e31199
--- /dev/null
+++ b/packages/js/e2e-utils-playwright/src/test/order.test.ts
@@ -0,0 +1,32 @@
+/**
+ * External dependencies
+ */
+import type { Page } from '@playwright/test';
+
+/**
+ * Internal dependencies
+ */
+import { getOrderIdFromUrl } from '../order';
+
+describe( 'getOrderIdFromUrl', () => {
+ it( 'should extract the order ID from a valid URL', () => {
+ const page = {
+ url: () => 'https://example.com/order-received/12345/',
+ } as unknown as Page;
+ expect( getOrderIdFromUrl( page ) ).toBe( '12345' );
+ } );
+
+ it( 'should return undefined if the URL does not contain an order ID', () => {
+ const page = {
+ url: () => 'https://example.com/order-received/',
+ } as unknown as Page;
+ expect( getOrderIdFromUrl( page ) ).toBeUndefined();
+ } );
+
+ it( 'should return undefined if the URL is not in the expected format', () => {
+ const page = {
+ url: () => 'https://example.com/other-page/12345/',
+ } as unknown as Page;
+ expect( getOrderIdFromUrl( page ) ).toBeUndefined();
+ } );
+} );
diff --git a/packages/js/e2e-utils-playwright/src/types.ts b/packages/js/e2e-utils-playwright/src/types.ts
new file mode 100644
index 0000000000..42d6e575be
--- /dev/null
+++ b/packages/js/e2e-utils-playwright/src/types.ts
@@ -0,0 +1,161 @@
+/**
+ * External dependencies
+ */
+import type { Page, FrameLocator } from '@playwright/test';
+import type { AxiosResponse } from 'axios';
+
+/**
+ * Authentication configuration for Basic Auth.
+ *
+ * @property {'basic'} type - Type of authentication ('basic')
+ * @property {string} username - Username for basic authentication
+ * @property {string} password - Password for basic authentication
+ */
+export interface BasicAuth {
+ type: 'basic';
+ username: string;
+ password: string;
+}
+
+/**
+ * Authentication configuration for OAuth 1.0a.
+ *
+ * @property {'oauth1'} type - Type of authentication ('oauth1')
+ * @property {string} consumerKey - OAuth1 consumer key
+ * @property {string} consumerSecret - OAuth1 consumer secret
+ */
+export interface OAuth1Auth {
+ type: 'oauth1';
+ consumerKey: string;
+ consumerSecret: string;
+}
+
+/**
+ * Union type for all supported authentication methods.
+ */
+export type Auth = BasicAuth | OAuth1Auth;
+
+/**
+ * API Client interface returned by createClient.
+ */
+export interface ApiClient {
+ /**
+ * Make a GET request.
+ *
+ * @template T - The expected response data type
+ * @param path - API endpoint path
+ * @param params - Query parameters
+ * @param debug - Enable debug logging
+ * @return Promise resolving to the response
+ */
+ get< T = unknown >(
+ path: string,
+ params?: Record< string, unknown >,
+ debug?: boolean
+ ): Promise< AxiosResponse< T > >;
+
+ /**
+ * Make a POST request.
+ *
+ * @template T - The expected response data type
+ * @param path - API endpoint path
+ * @param data - Request body data
+ * @param debug - Enable debug logging
+ * @return Promise resolving to the response
+ */
+ post< T = unknown >(
+ path: string,
+ data?: Record< string, unknown >,
+ debug?: boolean
+ ): Promise< AxiosResponse< T > >;
+
+ /**
+ * Make a PUT request.
+ *
+ * @template T - The expected response data type
+ * @param path - API endpoint path
+ * @param data - Request body data
+ * @param debug - Enable debug logging
+ * @return Promise resolving to the response
+ */
+ put< T = unknown >(
+ path: string,
+ data?: Record< string, unknown >,
+ debug?: boolean
+ ): Promise< AxiosResponse< T > >;
+
+ /**
+ * Make a DELETE request.
+ *
+ * @template T - The expected response data type
+ * @param path - API endpoint path
+ * @param params - Query parameters or request body
+ * @param debug - Enable debug logging
+ * @return Promise resolving to the response
+ */
+ delete< T = unknown >(
+ path: string,
+ params?: Record< string, unknown >,
+ debug?: boolean
+ ): Promise< AxiosResponse< T > >;
+}
+
+/**
+ * Checkout details for block-based checkout forms.
+ *
+ * @property {string} [country] - The country code (e.g., 'US')
+ * @property {string} [firstName] - The first name
+ * @property {string} [lastName] - The last name
+ * @property {string} [address] - The street address
+ * @property {string} [zip] - The ZIP or postal code
+ * @property {string} [city] - The city
+ * @property {string} [state] - The state
+ * @property {string} [suburb] - The suburb (for applicable countries)
+ * @property {string} [province] - The province (for applicable countries)
+ * @property {string} [district] - The district (for applicable countries)
+ * @property {string} [department] - The department (for applicable countries)
+ * @property {string} [region] - The region (for applicable countries)
+ * @property {string} [parish] - The parish (for applicable countries)
+ * @property {string} [county] - The county (for applicable countries)
+ * @property {string} [prefecture] - The prefecture (for applicable countries)
+ * @property {string} [municipality] - The municipality (for applicable countries)
+ * @property {string} [phone] - The phone number
+ * @property {boolean} [isPostalCode] - If true, search by 'Postal code' instead of 'ZIP Code'
+ */
+export interface CheckoutDetails {
+ country?: string;
+ firstName?: string;
+ lastName?: string;
+ address?: string;
+ zip?: string;
+ city?: string;
+ state?: string;
+ suburb?: string;
+ province?: string;
+ district?: string;
+ department?: string;
+ region?: string;
+ parish?: string;
+ county?: string;
+ prefecture?: string;
+ municipality?: string;
+ phone?: string;
+ isPostalCode?: boolean;
+}
+
+/**
+ * Address type for checkout forms.
+ */
+export type AddressType = 'shipping' | 'billing';
+
+/**
+ * Page context for editor functions that receive a page object.
+ */
+export interface PageContext {
+ page: Page;
+}
+
+/**
+ * Canvas type - either FrameLocator for iframe-based editor or Page.
+ */
+export type EditorCanvas = FrameLocator | Page;
diff --git a/packages/js/e2e-utils-playwright/tsconfig-cjs.json b/packages/js/e2e-utils-playwright/tsconfig-cjs.json
new file mode 100644
index 0000000000..f9d6e810b3
--- /dev/null
+++ b/packages/js/e2e-utils-playwright/tsconfig-cjs.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../tsconfig-cjs",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "build",
+ "typeRoots": ["./node_modules/@types"]
+ },
+ "include": ["src/**/*"],
+ "exclude": ["**/test/**"]
+}
diff --git a/packages/js/e2e-utils-playwright/tsconfig.json b/packages/js/e2e-utils-playwright/tsconfig.json
new file mode 100644
index 0000000000..5bb869c49e
--- /dev/null
+++ b/packages/js/e2e-utils-playwright/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../tsconfig",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "build-module",
+ "declaration": true,
+ "declarationMap": true,
+ "declarationDir": "./build-types",
+ "typeRoots": ["./node_modules/@types"]
+ },
+ "include": ["src/**/*"],
+ "exclude": ["**/test/**"]
+}
diff --git a/plugins/woocommerce/changelog/e2e-utils-playwright-ts-conversion b/plugins/woocommerce/changelog/e2e-utils-playwright-ts-conversion
new file mode 100644
index 0000000000..46f1a9c277
--- /dev/null
+++ b/plugins/woocommerce/changelog/e2e-utils-playwright-ts-conversion
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Convert e2e-utils-playwright package to TypeScript.
diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json
index 15d21fb460..edc851058a 100644
--- a/plugins/woocommerce/package.json
+++ b/plugins/woocommerce/package.json
@@ -959,6 +959,9 @@
"node_modules/@woocommerce/admin-library/build",
"node_modules/@woocommerce/block-library/build",
"node_modules/@woocommerce/classic-assets/build",
+ "node_modules/@woocommerce/e2e-utils-playwright/build",
+ "node_modules/@woocommerce/e2e-utils-playwright/build-module",
+ "node_modules/@woocommerce/e2e-utils-playwright/build-types",
"node_modules/@woocommerce/eslint-plugin/configs",
"node_modules/@woocommerce/eslint-plugin/rules",
"node_modules/@woocommerce/eslint-plugin/index.js"
diff --git a/plugins/woocommerce/tests/e2e-pw/utils/editor.js b/plugins/woocommerce/tests/e2e-pw/utils/editor.js
index 25d548e634..6dbd19fec9 100644
--- a/plugins/woocommerce/tests/e2e-pw/utils/editor.js
+++ b/plugins/woocommerce/tests/e2e-pw/utils/editor.js
@@ -1,4 +1,4 @@
-const { getCanvas } = require( '@woocommerce/e2e-utils-playwright/src' );
+const { getCanvas } = require( '@woocommerce/e2e-utils-playwright' );
const fillPageTitle = async ( page, title ) => {
// Close the Block Inserter if it's open.
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 908b3a21aa..9c4be92a62 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -98,7 +98,7 @@ importers:
version: 1.15.0
postcss-loader:
specifier: 4.3.x
- version: 4.3.0(postcss@8.4.49)(webpack@5.97.1(@swc/core@1.3.100))
+ version: 4.3.0(postcss@8.4.49)(webpack@5.97.1)
prettier:
specifier: npm:wp-prettier@^2.8.5
version: wp-prettier@2.8.5
@@ -177,7 +177,7 @@ importers:
version: 5.0.5
sass-loader:
specifier: 10.5.x
- version: 10.5.0(sass@1.69.5)(webpack@5.97.1)
+ version: 10.5.0(sass@1.69.5)(webpack@5.97.1(@swc/core@1.3.100))
ts-jest:
specifier: 29.1.x
version: 29.1.1(@babel/core@7.26.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.5.0(@types/node@22.9.1)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@22.9.1)(typescript@5.7.2)))(typescript@5.7.2)
@@ -259,7 +259,7 @@ importers:
version: 6.36.1-next.8b30e05b0.0
copy-webpack-plugin:
specifier: 13.0.x
- version: 13.0.0(webpack@5.97.1)
+ version: 13.0.0(webpack@5.97.1(@swc/core@1.3.100))
css-loader:
specifier: 6.11.x
version: 6.11.0(webpack@5.97.1(@swc/core@1.3.100))
@@ -292,7 +292,7 @@ importers:
version: 5.0.5
sass-loader:
specifier: 10.5.x
- version: 10.5.0(sass@1.69.5)(webpack@5.97.1)
+ version: 10.5.0(sass@1.69.5)(webpack@5.97.1(@swc/core@1.3.100))
ts-jest:
specifier: 29.1.x
version: 29.1.1(@babel/core@7.25.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.7))(jest@29.5.0(@types/node@22.9.1)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@22.9.1)(typescript@5.7.2)))(typescript@5.7.2)
@@ -602,7 +602,7 @@ importers:
version: 5.0.5
sass-loader:
specifier: 10.5.x
- version: 10.5.0(sass@1.69.5)(webpack@5.97.1)
+ version: 10.5.0(sass@1.69.5)(webpack@5.97.1(@swc/core@1.3.100))
ts-jest:
specifier: 29.1.x
version: 29.1.1(@babel/core@7.25.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.7))(jest@29.5.0(@types/node@22.9.1)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@22.9.1)(typescript@5.7.2)))(typescript@5.7.2)
@@ -845,7 +845,7 @@ importers:
version: 5.0.5
sass-loader:
specifier: 10.5.x
- version: 10.5.0(sass@1.69.5)(webpack@5.97.1)
+ version: 10.5.0(sass@1.69.5)(webpack@5.97.1(@swc/core@1.3.100))
ts-jest:
specifier: 29.1.x
version: 29.1.1(@babel/core@7.25.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.7))(jest@29.5.0(@types/node@22.9.1)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@22.9.1)(typescript@5.7.2)))(typescript@5.7.2)
@@ -1108,18 +1108,36 @@ importers:
specifier: ^2.2.6
version: 2.2.6
devDependencies:
- '@babel/cli':
- specifier: 7.25.7
- version: 7.25.7(@babel/core@7.25.7)
- '@babel/core':
- specifier: 7.25.7
- version: 7.25.7
- '@wordpress/babel-preset-default':
- specifier: next
- version: 8.36.1-next.8b30e05b0.0
+ '@playwright/test':
+ specifier: ^1.57.0
+ version: 1.57.0
+ '@types/jest':
+ specifier: 29.5.x
+ version: 29.5.14
+ '@types/node':
+ specifier: 20.x.x
+ version: 20.17.8
+ '@woocommerce/eslint-plugin':
+ specifier: workspace:*
+ version: link:../eslint-plugin
+ '@woocommerce/internal-js-tests':
+ specifier: workspace:*
+ version: link:../internal-js-tests
+ eslint:
+ specifier: ^8.55.0
+ version: 8.55.0
jest:
specifier: 29.5.x
version: 29.5.0(@types/node@20.17.8)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@20.17.8)(typescript@5.7.2))
+ jest-cli:
+ specifier: 29.5.x
+ version: 29.5.0(@types/node@20.17.8)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@20.17.8)(typescript@5.7.2))
+ ts-jest:
+ specifier: 29.1.x
+ version: 29.1.1(@babel/core@7.25.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.7))(jest@29.5.0(@types/node@20.17.8)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@20.17.8)(typescript@5.7.2)))(typescript@5.7.2)
+ typescript:
+ specifier: 5.7.x
+ version: 5.7.2
wireit:
specifier: 0.14.12
version: 0.14.12
@@ -1500,7 +1518,7 @@ importers:
version: 5.0.5
sass-loader:
specifier: 10.5.x
- version: 10.5.0(sass@1.69.5)(webpack@5.97.1)
+ version: 10.5.0(sass@1.69.5)(webpack@5.97.1(@swc/core@1.3.100))
ts-jest:
specifier: 29.1.x
version: 29.1.1(@babel/core@7.25.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.7))(jest@29.5.0(@types/node@22.9.1)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@22.9.1)(typescript@5.7.2)))(typescript@5.7.2)
@@ -1701,7 +1719,7 @@ importers:
version: 6.36.1-next.8b30e05b0.0
copy-webpack-plugin:
specifier: 13.0.x
- version: 13.0.0(webpack@5.97.1)
+ version: 13.0.0(webpack@5.97.1(@swc/core@1.3.100))
css-loader:
specifier: 6.11.x
version: 6.11.0(webpack@5.97.1(@swc/core@1.3.100))
@@ -1828,7 +1846,7 @@ importers:
version: 2.9.2(webpack@5.97.1(@swc/core@1.3.100))
postcss-loader:
specifier: 4.3.x
- version: 4.3.0(postcss@8.4.49)(webpack@5.97.1(@swc/core@1.3.100))
+ version: 4.3.0(postcss@8.4.49)(webpack@5.97.1)
sass-loader:
specifier: 10.5.x
version: 10.5.0(sass@1.69.5)(webpack@5.97.1(@swc/core@1.3.100))
@@ -2148,7 +2166,7 @@ importers:
version: 5.0.5
sass-loader:
specifier: 10.5.x
- version: 10.5.0(sass@1.69.5)(webpack@5.97.1)
+ version: 10.5.0(sass@1.69.5)(webpack@5.97.1(@swc/core@1.3.100))
ts-jest:
specifier: 29.1.x
version: 29.1.1(@babel/core@7.25.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.7))(jest@29.5.0(@types/node@22.9.1)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@22.9.1)(typescript@5.7.2)))(typescript@5.7.2)
@@ -2377,7 +2395,7 @@ importers:
version: 6.36.1-next.8b30e05b0.0
copy-webpack-plugin:
specifier: 13.0.x
- version: 13.0.0(webpack@5.97.1)
+ version: 13.0.0(webpack@5.97.1(@swc/core@1.3.100))
css-loader:
specifier: 6.11.x
version: 6.11.0(webpack@5.97.1(@swc/core@1.3.100))
@@ -2416,7 +2434,7 @@ importers:
version: 5.0.5
sass-loader:
specifier: 10.5.x
- version: 10.5.0(sass@1.69.5)(webpack@5.97.1)
+ version: 10.5.0(sass@1.69.5)(webpack@5.97.1(@swc/core@1.3.100))
ts-jest:
specifier: 29.1.x
version: 29.1.1(@babel/core@7.25.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.7))(jest@29.5.0(@types/node@22.9.1)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@22.9.1)(typescript@5.7.2)))(typescript@5.7.2)
@@ -2737,7 +2755,7 @@ importers:
version: 6.36.1-next.8b30e05b0.0
copy-webpack-plugin:
specifier: 13.0.x
- version: 13.0.0(webpack@5.97.1)
+ version: 13.0.0(webpack@5.97.1(@swc/core@1.3.100))
css-loader:
specifier: 6.11.x
version: 6.11.0(webpack@5.97.1(@swc/core@1.3.100))
@@ -2773,7 +2791,7 @@ importers:
version: 5.0.5
sass-loader:
specifier: 10.5.x
- version: 10.5.0(sass@1.69.5)(webpack@5.97.1)
+ version: 10.5.0(sass@1.69.5)(webpack@5.97.1(@swc/core@1.3.100))
ts-jest:
specifier: 29.1.x
version: 29.1.1(@babel/core@7.25.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.7))(jest@29.5.0(@types/node@22.9.1)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@22.9.1)(typescript@5.7.2)))(typescript@5.7.2)
@@ -3515,7 +3533,7 @@ importers:
version: 3.3.7
copy-webpack-plugin:
specifier: 13.0.x
- version: 13.0.0(webpack@5.97.1)
+ version: 13.0.0(webpack@5.97.1(@swc/core@1.3.100))
css-loader:
specifier: 6.11.x
version: 6.11.0(webpack@5.97.1(@swc/core@1.3.100))
@@ -3614,7 +3632,7 @@ importers:
version: 1.69.5
sass-loader:
specifier: 10.5.x
- version: 10.5.0(sass@1.69.5)(webpack@5.97.1)
+ version: 10.5.0(sass@1.69.5)(webpack@5.97.1(@swc/core@1.3.100))
stylelint:
specifier: ^14.16.1
version: 14.16.1
@@ -4072,7 +4090,7 @@ importers:
version: 5.2.2(webpack@5.97.1)
copy-webpack-plugin:
specifier: 13.0.x
- version: 13.0.0(webpack@5.97.1)
+ version: 13.0.0(webpack@5.97.1(@swc/core@1.3.100))
core-js:
specifier: 3.25.0
version: 3.25.0
@@ -4201,7 +4219,7 @@ importers:
version: 4.1.1
sass-loader:
specifier: 10.5.x
- version: 10.5.0(sass@1.69.5)(webpack@5.97.1)
+ version: 10.5.0(sass@1.69.5)(webpack@5.97.1(@swc/core@1.3.100))
storybook:
specifier: ^7.6.4
version: 7.6.4(encoding@0.1.13)
@@ -4547,7 +4565,7 @@ importers:
version: link:../../packages/js/eslint-plugin
copy-webpack-plugin:
specifier: 13.0.x
- version: 13.0.0(webpack@5.97.1)
+ version: 13.0.0(webpack@5.97.1(@swc/core@1.3.100))
eslint:
specifier: ^8.55.0
version: 8.55.0
@@ -26577,7 +26595,7 @@ snapshots:
'@types/istanbul-lib-coverage': 2.0.6
collect-v8-coverage: 1.0.2
- '@jest/test-sequencer@26.6.3':
+ '@jest/test-sequencer@26.6.3(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@22.9.1)(typescript@5.7.2))':
dependencies:
'@jest/test-result': 26.6.2
graceful-fs: 4.2.11
@@ -26585,7 +26603,11 @@ snapshots:
jest-runner: 26.6.3(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@22.9.1)(typescript@5.7.2))
jest-runtime: 26.6.3(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@22.9.1)(typescript@5.7.2))
transitivePeerDependencies:
+ - bufferutil
+ - canvas
- supports-color
+ - ts-node
+ - utf-8-validate
'@jest/test-sequencer@29.7.0':
dependencies:
@@ -31159,7 +31181,7 @@ snapshots:
'@tufjs/models@1.0.4':
dependencies:
'@tufjs/canonical-json': 1.0.0
- minimatch: 9.0.3
+ minimatch: 9.0.5
'@tybys/wasm-util@0.9.0':
dependencies:
@@ -32204,7 +32226,7 @@ snapshots:
'@typescript-eslint/utils@5.56.0(eslint@8.55.0)(typescript@5.7.2)':
dependencies:
- '@eslint-community/eslint-utils': 4.4.0(eslint@8.55.0)
+ '@eslint-community/eslint-utils': 4.7.0(eslint@8.55.0)
'@types/json-schema': 7.0.15
'@types/semver': 7.5.6
'@typescript-eslint/scope-manager': 5.56.0
@@ -32234,7 +32256,7 @@ snapshots:
'@typescript-eslint/utils@6.21.0(eslint@8.55.0)(typescript@5.7.2)':
dependencies:
- '@eslint-community/eslint-utils': 4.4.0(eslint@8.55.0)
+ '@eslint-community/eslint-utils': 4.7.0(eslint@8.55.0)
'@types/json-schema': 7.0.15
'@types/semver': 7.5.6
'@typescript-eslint/scope-manager': 6.21.0
@@ -32548,7 +32570,7 @@ snapshots:
'@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.97.1)':
dependencies:
- webpack: 5.97.1(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@5.1.4)
+ webpack: 5.97.1(@swc/core@1.3.100)(webpack-cli@5.1.4)
webpack-cli: 5.1.4(webpack-bundle-analyzer@4.9.1)(webpack-dev-server@4.15.1)(webpack@5.97.1)
'@webpack-cli/info@1.5.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0))':
@@ -32558,7 +32580,7 @@ snapshots:
'@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.97.1)':
dependencies:
- webpack: 5.97.1(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@5.1.4)
+ webpack: 5.97.1(@swc/core@1.3.100)(webpack-cli@5.1.4)
webpack-cli: 5.1.4(webpack-bundle-analyzer@4.9.1)(webpack-dev-server@4.15.1)(webpack@5.97.1)
'@webpack-cli/serve@1.7.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0))':
@@ -32567,10 +32589,10 @@ snapshots:
'@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack-dev-server@4.15.1)(webpack@5.97.1)':
dependencies:
- webpack: 5.97.1(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@5.1.4)
+ webpack: 5.97.1(@swc/core@1.3.100)(webpack-cli@5.1.4)
webpack-cli: 5.1.4(webpack-bundle-analyzer@4.9.1)(webpack-dev-server@4.15.1)(webpack@5.97.1)
optionalDependencies:
- webpack-dev-server: 4.15.1(webpack-cli@5.1.4)(webpack@5.97.1)
+ webpack-dev-server: 4.15.1(debug@4.3.4)(webpack-cli@5.1.4)(webpack@5.97.1)
'@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack@5.97.1)':
dependencies:
@@ -36825,7 +36847,7 @@ snapshots:
expect-puppeteer: 4.4.0
filenamify: 4.3.0
jest: 26.6.3(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@22.9.1)(typescript@5.7.2))
- jest-circus: 26.6.3
+ jest-circus: 26.6.3(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@22.9.1)(typescript@5.7.2))
jest-dev-server: 5.0.3
jest-environment-node: 26.6.2
markdownlint: 0.23.1
@@ -39853,15 +39875,6 @@ snapshots:
tinyglobby: 0.2.12
webpack: 5.97.1(@swc/core@1.3.100)(webpack-cli@5.1.4)
- copy-webpack-plugin@13.0.0(webpack@5.97.1):
- dependencies:
- glob-parent: 6.0.2
- normalize-path: 3.0.0
- schema-utils: 4.3.0
- serialize-javascript: 6.0.2
- tinyglobby: 0.2.12
- webpack: 5.97.1(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@5.1.4)
-
core-js-compat@3.39.0:
dependencies:
browserslist: 4.24.4
@@ -42010,7 +42023,7 @@ snapshots:
eslint@8.55.0:
dependencies:
- '@eslint-community/eslint-utils': 4.4.0(eslint@8.55.0)
+ '@eslint-community/eslint-utils': 4.7.0(eslint@8.55.0)
'@eslint-community/regexpp': 4.10.0
'@eslint/eslintrc': 2.1.4
'@eslint/js': 8.55.0
@@ -43991,7 +44004,7 @@ snapshots:
ignore-walk@6.0.4:
dependencies:
- minimatch: 9.0.3
+ minimatch: 9.0.5
ignore@4.0.6: {}
@@ -44601,7 +44614,7 @@ snapshots:
jest-util: 29.7.0
p-limit: 3.1.0
- jest-circus@26.6.3:
+ jest-circus@26.6.3(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@22.9.1)(typescript@5.7.2)):
dependencies:
'@babel/traverse': 7.25.9
'@jest/environment': 26.6.2
@@ -44625,7 +44638,11 @@ snapshots:
stack-utils: 2.0.6
throat: 5.0.0
transitivePeerDependencies:
+ - bufferutil
+ - canvas
- supports-color
+ - ts-node
+ - utf-8-validate
jest-circus@29.5.0:
dependencies:
@@ -44789,7 +44806,7 @@ snapshots:
jest-config@26.6.3(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@22.9.1)(typescript@5.7.2)):
dependencies:
'@babel/core': 7.25.7
- '@jest/test-sequencer': 26.6.3
+ '@jest/test-sequencer': 26.6.3(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@22.9.1)(typescript@5.7.2))
'@jest/types': 26.6.2
babel-jest: 26.6.3(@babel/core@7.25.7)
chalk: 4.1.2
@@ -48472,16 +48489,6 @@ snapshots:
semver: 7.6.3
webpack: 4.47.0(webpack-cli@5.1.4)
- postcss-loader@4.3.0(postcss@8.4.49)(webpack@5.97.1(@swc/core@1.3.100)):
- dependencies:
- cosmiconfig: 7.1.0
- klona: 2.0.6
- loader-utils: 2.0.4
- postcss: 8.4.49
- schema-utils: 3.3.0
- semver: 7.6.3
- webpack: 5.97.1(@swc/core@1.3.100)(webpack-cli@5.1.4)
-
postcss-loader@4.3.0(postcss@8.4.49)(webpack@5.97.1):
dependencies:
cosmiconfig: 7.1.0
@@ -48490,7 +48497,7 @@ snapshots:
postcss: 8.4.49
schema-utils: 3.3.0
semver: 7.6.3
- webpack: 5.97.1(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@5.1.4)
+ webpack: 5.97.1(@swc/core@1.3.100)(webpack-cli@5.1.4)
postcss-loader@6.2.1(postcss@8.4.32)(webpack@5.89.0):
dependencies:
@@ -50677,17 +50684,6 @@ snapshots:
optionalDependencies:
sass: 1.69.5
- sass-loader@10.5.0(sass@1.69.5)(webpack@5.97.1):
- dependencies:
- klona: 2.0.6
- loader-utils: 2.0.4
- neo-async: 2.6.2
- schema-utils: 3.3.0
- semver: 7.6.3
- webpack: 5.97.1(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@5.1.4)
- optionalDependencies:
- sass: 1.69.5
-
sass-loader@12.6.0(sass@1.69.5)(webpack@5.89.0):
dependencies:
klona: 2.0.6
@@ -53388,11 +53384,11 @@ snapshots:
import-local: 3.1.0
interpret: 3.1.1
rechoir: 0.8.0
- webpack: 5.97.1(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@5.1.4)
+ webpack: 5.97.1(@swc/core@1.3.100)(webpack-cli@5.1.4)
webpack-merge: 5.10.0
optionalDependencies:
webpack-bundle-analyzer: 4.9.1
- webpack-dev-server: 4.15.1(webpack-cli@5.1.4)(webpack@5.97.1)
+ webpack-dev-server: 4.15.1(debug@4.3.4)(webpack-cli@5.1.4)(webpack@5.97.1)
webpack-cli@5.1.4(webpack@5.97.1):
dependencies: