Merge branch 'trunk' into add/wooexpress-rin-rule

This commit is contained in:
Panos (Panagiotis) Synetos 2023-11-06 14:46:53 +02:00
commit 53d7388269
No known key found for this signature in database
GPG Key ID: 0404F1D7F00137F9
51 changed files with 1157 additions and 89 deletions

26
.github/workflows/milestoned.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: Milestone Manager
on:
pull_request_target:
types: [milestoned]
permissions: {}
jobs:
remove-milestone-from-unmerged-prs:
name: "Remove Milestone from Unmerged PRs"
if: github.event.pull_request.merged != true
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/github-script@v6
with:
script: |
github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
milestone: null,
});

View File

@ -38,9 +38,7 @@ jobs:
with:
php-version: '7.4'
- name: 'Run the script to assign a milestone'
if: |
!github.event.pull_request.milestone &&
github.event.pull_request.base.ref == 'trunk'
if: github.event.pull_request.base.ref == 'trunk'
run: php assign-milestone-to-merged-pr.php
env:
PULL_REQUEST_ID: ${{ github.event.pull_request.node_id }}

29
.github/workflows/scripts/stalebot.js vendored Normal file
View File

@ -0,0 +1,29 @@
/**
* Set the stalebot start date given a cron schedule.
*/
// You need to install this dependency as part of your workflow.
const core = require( '@actions/core' );
const ScheduleStartDate = () => {
let scheduleStartDate;
switch ( process.env.CRON_SCHEDULE ) {
case '21 1 * * *':
scheduleStartDate = '2022-01-01';
break;
case '31 2 * * *':
scheduleStartDate = '2023-01-01';
break;
case '41 3 * * *':
scheduleStartDate = '2023-08-01';
break;
default:
scheduleStartDate = '2018-01-01';
break;
}
core.setOutput( 'stale-start-date', scheduleStartDate );
};
ScheduleStartDate();

View File

@ -1,7 +1,10 @@
name: 'Close stale needs-feedback issues'
name: 'Process stale needs-feedback issues'
on:
schedule:
- cron: '21 0 * * *'
- cron: '11 0 * * *'
- cron: '21 1 * * *'
- cron: '31 2 * * *'
- cron: '41 3 * * *'
permissions: {}
@ -13,10 +16,20 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v8
- name: Install Actions Core
run: npm --prefix .github/workflows/scripts install @actions/core
- name: Get start date
id: startdate
run: node .github/workflows/scripts/stalebot.js
env:
CRON_SCHEDULE: ${{ github.event.schedule }}
- name: Scan issues
uses: actions/stale@v8
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
operations-per-run: 40
operations-per-run: 8
start-date: steps.startdate.outputs.stale-start-date
stale-issue-message: "As a part of this repository's maintenance, this issue is being marked as stale due to inactivity. Please feel free to comment on it in case we missed something.\n\n###### After 7 days with no activity this issue will be automatically be closed."
close-issue-message: 'This issue was closed because it has been 14 days with no activity.'
days-before-issue-stale: 7

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Add preview and replace button to downloads edit #40835

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix selection in currency and number fields to only select if field still has focus.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Allow plugins to access PostTypeContext and blocks (through core/block-editor data store).

View File

@ -1,13 +1,17 @@
/**
* External dependencies
*/
import { ChangeEvent } from 'react';
import { __, sprintf } from '@wordpress/i18n';
import { createElement, useState } from '@wordpress/element';
import { trash } from '@wordpress/icons';
import { useDispatch } from '@wordpress/data';
import { useDispatch, useSelect } from '@wordpress/data';
import { recordEvent } from '@woocommerce/tracks';
import { ImageGallery, ImageGalleryItem } from '@woocommerce/components';
import { uploadMedia } from '@wordpress/media-utils';
import {
Button,
FormFileUpload,
Modal,
BaseControl,
// @ts-expect-error `__experimentalInputControl` does exist.
@ -20,18 +24,39 @@ import {
import { EditDownloadsModalProps } from './types';
import { UnionIcon } from './union-icon';
export interface Image {
id: number;
src: string;
name: string;
alt: string;
}
export const EditDownloadsModal: React.FC< EditDownloadsModalProps > = ( {
downloableItem,
maxUploadFileSize = 10000000,
onCancel,
onChange,
onRemove,
onSave,
onUploadSuccess,
onUploadError,
} ) => {
const { createNotice } = useDispatch( 'core/notices' );
const [ isCopingToClipboard, setIsCopingToClipboard ] =
useState< boolean >( false );
const [ isFileUploading, setIsFileUploading ] =
useState< boolean >( false );
const { file = '', name = '' } = downloableItem;
const { allowedMimeTypes } = useSelect( ( select ) => {
const { getEditorSettings } = select( 'core/editor' );
return getEditorSettings();
} );
const allowedTypes = allowedMimeTypes
? Object.values( allowedMimeTypes )
: [];
const { id = 0, file = '', name = '' } = downloableItem;
const onCopySuccess = () => {
createNotice(
@ -40,6 +65,15 @@ export const EditDownloadsModal: React.FC< EditDownloadsModalProps > = ( {
);
};
const isImage = ( filename = '' ) => {
if ( ! filename ) return;
const imageExtensions = [ 'jpg', 'jpeg', 'png', 'gif', 'webp' ];
const fileExtension = (
filename.split( '.' ).pop() || ''
).toLowerCase();
return imageExtensions.includes( fileExtension );
};
async function copyTextToClipboard( text: string ) {
if ( 'clipboard' in navigator ) {
await navigator.clipboard.writeText( text );
@ -61,6 +95,21 @@ export const EditDownloadsModal: React.FC< EditDownloadsModalProps > = ( {
setIsCopingToClipboard( false );
}
async function handleFormFileUploadChange(
event: ChangeEvent< HTMLInputElement >
) {
setIsFileUploading( true );
const filesList = event.currentTarget.files as FileList;
await uploadMedia( {
allowedTypes,
filesList,
maxUploadFileSize,
onFileChange: onUploadSuccess,
onError: onUploadError,
} );
setIsFileUploading( false );
}
return (
<Modal
title={ sprintf(
@ -81,6 +130,34 @@ export const EditDownloadsModal: React.FC< EditDownloadsModalProps > = ( {
} }
className="woocommerce-edit-downloads-modal"
>
<div className="woocommerce-edit-downloads-modal__preview">
{ isImage( file ) && (
<ImageGallery allowDragging={ false } columns={ 1 }>
<ImageGalleryItem
key={ id }
alt={ name }
src={ file }
id={ `${ id }` }
isCover={ false }
/>
</ImageGallery>
) }
<FormFileUpload
onChange={ handleFormFileUploadChange }
render={ ( { openFileDialog } ) => (
<div>
<p>{ name }</p>
<Button
onClick={ openFileDialog }
isBusy={ isFileUploading }
disabled={ isFileUploading }
>
{ __( 'Replace', 'woocommerce' ) }
</Button>
</div>
) }
/>
</div>
<BaseControl
id={ 'file-name-help' }
className="woocommerce-edit-downloads-modal__file-name"

View File

@ -1,3 +1,5 @@
$gutenberg-blue: var(--wp-admin-theme-color);
.woocommerce-edit-downloads-modal {
&__buttons {
display: flex;
@ -15,7 +17,37 @@
padding-bottom: $gap-large;
}
&__preview {
margin-bottom: $gap;
}
.components-input-control__suffix {
cursor: pointer;
}
.woocommerce-image-gallery,
.components-form-file-upload {
display: inline-block;
vertical-align: middle;
padding-right: $gap;
p {
margin-bottom: $gap-smallest;
}
button {
color: $gutenberg-blue;
padding: 0;
}
button:hover:not(.is-busy) {
background: rgba(var(--wp-admin-theme-color--rgb), 0.04);
}
.woocommerce-image-gallery__item {
width: $gap-larger * 2;
height: $gap-larger * 2;
img {
width: $gap-larger * 2;
height: $gap-larger * 2;
border-color: #fff;
}
}
}
}

View File

@ -2,11 +2,15 @@
* External dependencies
*/
import { ProductDownload } from '@woocommerce/data';
import { MediaItem } from '@wordpress/media-utils';
export type EditDownloadsModalProps = {
downloableItem: ProductDownload;
maxUploadFileSize?: number;
onCancel: () => void;
onRemove: () => void;
onSave: () => void;
onChange: ( name: string ) => void;
onUploadSuccess( files: MediaItem[] ): void;
onUploadError( error: unknown ): void;
};

View File

@ -143,6 +143,39 @@ export function Edit( {
}
}
function handleFileReplace( files: MediaItem | MediaItem[] ) {
if (
! Array.isArray( files ) ||
! files?.length ||
files[ 0 ]?.id === undefined
) {
return;
}
if ( ! downloads.length ) {
setDownloadable( true );
}
const uploadedFile = {
id: stringifyId( files[ 0 ].id ),
file: files[ 0 ].url,
name:
files[ 0 ].title ||
files[ 0 ].alt ||
files[ 0 ].caption ||
getFileName( files[ 0 ].url ),
};
const stringifyIds = downloads.map( ( download ) => {
if ( download.file === selectedDownload?.file ) {
return stringifyEntityId( uploadedFile );
}
return stringifyEntityId( download );
} );
setDownloads( stringifyIds );
setSelectedDownload( uploadedFile );
}
function removeDownload( download: ProductDownload ) {
const otherDownloads = downloads.reduce< ProductDownload[] >(
function removeDownloadElement(
@ -336,6 +369,8 @@ export function Edit( {
setDownloads( newDownloads );
setSelectedDownload( null );
} }
onUploadSuccess={ handleFileReplace }
onUploadError={ handleUploadError }
/>
) }
</div>

View File

@ -5,6 +5,7 @@ import { synchronizeBlocksWithTemplate, Template } from '@wordpress/blocks';
import { createElement, useMemo, useLayoutEffect } from '@wordpress/element';
import { useDispatch, useSelect, select as WPSelect } from '@wordpress/data';
import { uploadMedia } from '@wordpress/media-utils';
import { PluginArea } from '@wordpress/plugins';
import {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
@ -32,6 +33,7 @@ import {
*/
import { useConfirmUnsavedProductChanges } from '../../hooks/use-confirm-unsaved-product-changes';
import { ProductEditorContext } from '../../types';
import { PostTypeContext } from '../../contexts/post-type-context';
type BlockEditorProps = {
context: Partial< ProductEditorContext >;
@ -120,6 +122,11 @@ export function BlockEditor( {
<BlockList className="woocommerce-product-block-editor__block-list" />
</ObserveTyping>
</BlockTools>
{ /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ }
<PostTypeContext.Provider value={ context.postType! }>
{ /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ }
<PluginArea scope="woocommerce-product-block-editor" />
</PostTypeContext.Provider>
</BlockEditorProvider>
</BlockContextProvider>
</div>

View File

@ -7,7 +7,6 @@ import {
Fragment,
useState,
} from '@wordpress/element';
import { PluginArea } from '@wordpress/plugins';
import {
LayoutContextProvider,
useExtendLayout,
@ -90,8 +89,6 @@ export function Editor( {
postId: product.id,
} }
/>
{ /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ }
<PluginArea scope="woocommerce-product-block-editor" />
</>
}
/>

View File

@ -0,0 +1,6 @@
/**
* External dependencies
*/
import { createContext } from '@wordpress/element';
export const PostTypeContext = createContext( 'product' );

View File

@ -8,7 +8,7 @@ import { useContext } from '@wordpress/element';
* Internal dependencies
*/
import { useProductHelper } from './use-product-helper';
import { formatCurrencyDisplayValue } from '../utils';
import { deferSelectInFocus, formatCurrencyDisplayValue } from '../utils';
export type CurrencyInputProps = {
prefix: string;
@ -51,18 +51,7 @@ export const useCurrencyInputProps = ( {
return sanitizePrice( String( val ) );
},
onFocus( event: React.FocusEvent< HTMLInputElement > ) {
// In some browsers like safari .select() function inside
// the onFocus event doesn't work as expected because it
// conflicts with onClick the first time user click the
// input. Using setTimeout defers the text selection and
// avoid the unexpected behaviour.
setTimeout(
function deferSelection( element: HTMLInputElement ) {
element.select();
},
0,
event.currentTarget
);
deferSelectInFocus( event.currentTarget );
if ( onFocus ) {
onFocus( event );
}

View File

@ -2,6 +2,7 @@
* Internal dependencies
*/
import { useProductHelper } from './use-product-helper';
import { deferSelectInFocus } from '../utils';
export type NumberInputProps = {
value: string;
@ -28,18 +29,7 @@ export const useNumberInputProps = ( {
const numberInputProps: NumberInputProps = {
value: formatNumber( value ),
onFocus( event: React.FocusEvent< HTMLInputElement > ) {
// In some browsers like safari .select() function inside
// the onFocus event doesn't work as expected because it
// conflicts with onClick the first time user click the
// input. Using setTimeout defers the text selection and
// avoid the unexpected behaviour.
setTimeout(
function deferSelection( element: HTMLInputElement ) {
element.select();
},
0,
event.currentTarget
);
deferSelectInFocus( event.currentTarget );
if ( onFocus ) {
onFocus( event );
}

View File

@ -20,5 +20,6 @@ export * from './utils';
* Hooks
*/
export * from './hooks';
export { PostTypeContext } from './contexts/post-type-context';
export { useValidation, useValidations } from './contexts/validation-context';
export * from './contexts/validation-context/types';

View File

@ -0,0 +1,17 @@
export function deferSelectInFocus( element: HTMLInputElement ) {
// In some browsers like safari .select() function inside
// the onFocus event doesn't work as expected because it
// conflicts with onClick the first time user click the
// input. Using setTimeout defers the text selection and
// avoid the unexpected behaviour.
setTimeout(
function deferSelection( originalElement: HTMLInputElement ) {
if ( element.ownerDocument.activeElement === originalElement ) {
// We still have focus, so select the content.
originalElement.select();
}
},
0,
element
);
}

View File

@ -2,6 +2,7 @@
* Internal dependencies
*/
import { AUTO_DRAFT_NAME } from './constants';
import { deferSelectInFocus } from './defer-select-in-focus';
import { formatCurrencyDisplayValue } from './format-currency-display-value';
import { getCheckboxTracks } from './get-checkbox-tracks';
import { getCurrencySymbolProps } from './get-currency-symbol-props';
@ -30,6 +31,7 @@ export * from './sift';
export {
AUTO_DRAFT_NAME,
deferSelectInFocus,
formatCurrencyDisplayValue,
getCheckboxTracks,
getCurrencySymbolProps,

View File

@ -66,7 +66,7 @@ const MAX_PAGE_COUNT = 100;
export const BlockEditor = ( {} ) => {
const history = useHistory();
const settings = useSiteEditorSettings();
const [ blocks, onChange ] = useEditorBlocks();
const [ blocks, , onChange ] = useEditorBlocks();
const urlParams = useQuery();
const { currentState } = useContext( CustomizeStoreContext );

View File

@ -145,35 +145,15 @@ export const Layout = () => {
</NavigableRegion>
{ ! isMobileViewport && (
<div
className={ classnames(
'edit-site-layout__canvas-container'
) }
>
<div className="edit-site-layout__canvas-container">
{ canvasResizer }
{ !! canvasSize.width && (
<motion.div
whileHover={ {
scale: 1.005,
transition: {
duration: disableMotion
? 0
: 0.5,
ease: 'easeOut',
},
} }
initial={ false }
layout="position"
className={ classnames(
'edit-site-layout__canvas'
) }
transition={ {
type: 'tween',
duration: disableMotion
? 0
: ANIMATION_DURATION,
ease: 'easeOut',
} }
>
<ErrorBoundary>
<ResizableFrame

View File

@ -158,7 +158,7 @@ export const OnboardingTour = ( {
[ key: string ]: unknown;
} ) => {
if ( placement === 'left' ) {
return [ -15, 35 ];
return [ 0, 20 ];
}
return [ 52, 16 ];
},

View File

@ -189,12 +189,10 @@ function ResizableFrame( {
},
};
const currentResizeHandleVariant = ( () => {
if ( isResizing ) {
if ( isResizing || isHandleVisibleByDefault ) {
return 'active';
}
return shouldShowHandle || isHandleVisibleByDefault
? 'visible'
: 'hidden';
return shouldShowHandle ? 'visible' : 'hidden';
} )();
const resizeHandler = (
@ -246,6 +244,13 @@ function ResizableFrame( {
if ( definition === 'fullWidth' )
setFrameSize( { width: '100%', height: '100%' } );
} }
whileHover={ {
scale: 1.005,
transition: {
duration: 0.5,
ease: 'easeOut',
},
} }
transition={ frameTransition }
size={ frameSize }
enable={ {

View File

@ -1187,6 +1187,13 @@ export const COLOR_PALETTES = [
text: 'var(--wp--preset--color--background)',
},
},
':visited': {
color: {
text: color.styles.elements?.button
? color.styles.elements.button.color
: 'var(--wp--preset--color--background)',
},
},
color: {
background: 'var(--wp--preset--color--primary)',
text: 'var(--wp--preset--color--background)',

View File

@ -26,8 +26,17 @@ export const ColorPanel = () => {
const [ rawSettings ] = useGlobalSetting( '' );
const settings = useSettingsForBlockElement( rawSettings );
const onChange = ( ...props ) => {
setStyle( ...props );
const onChange = ( _style ) => {
setStyle( {
..._style,
blocks: {
..._style.blocks,
// Reset the "core/button" color that may have been set via predefined color palette to ensure it uses the custom button color.
'core/button': {
color: {},
},
},
} );
setUserConfig( ( currentConfig ) => ( {
...currentConfig,
settings: mergeBaseAndUserConfigs( currentConfig.settings, {

View File

@ -4,10 +4,14 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { createInterpolateElement } from '@wordpress/element';
import { createInterpolateElement, useContext } from '@wordpress/element';
import { Link } from '@woocommerce/components';
import { PanelBody } from '@wordpress/components';
import { recordEvent } from '@woocommerce/tracks';
// @ts-ignore No types for this exist yet.
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
/**
* Internal dependencies
@ -16,7 +20,14 @@ import { SidebarNavigationScreen } from './sidebar-navigation-screen';
import { ADMIN_URL } from '~/utils/admin-settings';
import { ColorPalette, ColorPanel } from './global-styles';
const { GlobalStylesContext } = unlock( blockEditorPrivateApis );
const SidebarNavigationScreenColorPaletteContent = () => {
// @ts-ignore No types for this exist yet.
const { user } = useContext( GlobalStylesContext );
const hasCreatedOwnColors = !! (
user.settings.color && user.settings.color.palette.hasCreatedOwnColors
);
// Wrap in a BlockEditorProvider to ensure that the Iframe's dependencies are
// loaded. This is necessary because the Iframe component waits until
// the block editor store's `__internalIsInitialized` is true before
@ -34,7 +45,7 @@ const SidebarNavigationScreenColorPaletteContent = () => {
<PanelBody
className="woocommerce-customize-store__color-panel-container"
title={ __( 'or create your own', 'woocommerce' ) }
initialOpen={ false }
initialOpen={ hasCreatedOwnColors }
>
<ColorPanel />
</PanelBody>

View File

@ -29,7 +29,7 @@ import { findPatternByBlock } from './utils';
import BlockPatternList from '../block-pattern-list';
const SUPPORTED_FOOTER_PATTERNS = [
'woocommerce-blocks/footer-simple-menu-and-cart',
'woocommerce-blocks/footer-simple-menu',
'woocommerce-blocks/footer-with-3-menus',
'woocommerce-blocks/footer-large',
];

View File

@ -415,6 +415,11 @@
color: $gray-900;
}
}
.color-block-support-panel {
border-top: 0;
padding: 0;
}
}
.woocommerce-customize-store_color-palette-container {

View File

@ -116,8 +116,7 @@ export const ApiCallLoader = () => {
return (
<Loader>
<Loader.Sequence
/* divide all frames equally over 1m. */
interval={ ( 60 * 1000 ) / ( loaderSteps.length - 1 ) }
interval={ ( 40 * 1000 ) / ( loaderSteps.length - 1 ) }
shouldLoop={ false }
>
{ loaderSteps.slice( 0, -1 ).map( ( step, index ) => (

View File

@ -5,8 +5,8 @@ import { z } from 'zod';
const footerChoices = [
{
slug: 'woocommerce-blocks/footer-simple-menu-and-cart',
label: 'Footer with Simple Menu and Cart',
slug: 'woocommerce-blocks/footer-simple-menu',
label: 'Footer with Simple Menu',
},
{
slug: 'woocommerce-blocks/footer-with-3-menus',

View File

@ -218,11 +218,11 @@ export const updateStorePatterns = async (
woocommerce_blocks_allow_ai_connection: true,
} );
const response: {
const { images } = await apiFetch< {
ai_content_generated: boolean;
additional_errors?: unknown[];
} = await apiFetch( {
path: '/wc/store/patterns',
images: Array< unknown >;
} >( {
path: '/wc/private/ai/images',
method: 'POST',
data: {
business_description:
@ -230,6 +230,63 @@ export const updateStorePatterns = async (
},
} );
const [ response ] = await Promise.all< {
ai_content_generated: boolean;
product_content: Array< {
title: string;
description: string;
image: {
src: string;
alt: string;
};
} >;
additional_errors?: unknown[];
} >( [
apiFetch( {
path: '/wc/private/ai/products',
method: 'POST',
data: {
business_description:
context.businessInfoDescription.descriptionText,
images,
},
} ),
apiFetch( {
path: '/wc/private/ai/patterns',
method: 'POST',
data: {
business_description:
context.businessInfoDescription.descriptionText,
images,
},
} ),
] );
const productContents = response.product_content.map(
( product, index ) => {
return apiFetch( {
path: '/wc/private/ai/product',
method: 'POST',
data: {
products_information: product,
index,
},
} );
}
);
await Promise.all( [
...productContents,
apiFetch( {
path: '/wc/private/ai/business-description',
method: 'POST',
data: {
business_description:
context.businessInfoDescription.descriptionText,
},
} ),
] );
if ( ! response.ai_content_generated ) {
throw new Error(
'AI content not generated: ' + response.additional_errors
@ -260,6 +317,12 @@ const updateGlobalStyles = async ( {
( pairing ) => pairing.title === fontPairingName
);
// @ts-ignore No types for this exist yet.
const { invalidateResolutionForStoreSelector } = dispatch( coreStore );
invalidateResolutionForStoreSelector(
'__experimentalGetCurrentGlobalStylesId'
);
const globalStylesId = await resolveSelect(
coreStore
// @ts-ignore No types for this exist yet.
@ -299,6 +362,7 @@ const updateTemplate = async ( {
// Ensure that the patterns are up to date because we populate images and content in previous step.
invalidateResolutionForStoreSelector( 'getBlockPatterns' );
invalidateResolutionForStoreSelector( '__experimentalGetTemplateForLink' );
const patterns = ( await resolveSelect(
coreStore
@ -349,7 +413,6 @@ export const assembleSite = async (
} );
recordEvent( 'customize_your_store_ai_update_global_styles_success' );
} catch ( error ) {
// TODO handle error
// eslint-disable-next-line no-console
console.error( error );
recordEvent(
@ -358,6 +421,7 @@ export const assembleSite = async (
error: error instanceof Error ? error.message : 'unknown',
}
);
throw error;
}
try {
@ -368,12 +432,12 @@ export const assembleSite = async (
} );
recordEvent( 'customize_your_store_ai_update_template_success' );
} catch ( error ) {
// TODO handle error
// eslint-disable-next-line no-console
console.error( error );
recordEvent( 'customize_your_store_ai_update_template_response_error', {
error: error instanceof Error ? error.message : 'unknown',
} );
throw error;
}
};

View File

@ -58,7 +58,7 @@ export type Campaign = {
cost: {
value: string;
currency: string;
};
} | null;
};
export type CampaignsPage = {

View File

@ -46,11 +46,15 @@ export const useCampaigns = ( page = 1, perPage = 5 ): UseCampaignsType => {
( el ) => el.slug === campaign.channel
);
const cost = campaign.cost
? `${ campaign.cost.currency } ${ campaign.cost.value }`
: '';
return {
id: `${ campaign.channel }|${ campaign.id }`,
title: campaign.title,
description: '',
cost: `${ campaign.cost.currency } ${ campaign.cost.value }`,
cost,
manageUrl: campaign.manage_url,
icon: channel?.icon || '',
channelName: channel?.title || '',

View File

@ -20,6 +20,7 @@ import {
TablePlaceholder,
Link,
} from '@woocommerce/components';
import { isWCAdmin } from '@woocommerce/navigation';
/**
* Internal dependencies
@ -138,7 +139,16 @@ export const Campaigns = () => {
<FlexBlock>
<Flex direction="column" gap={ 1 }>
<FlexItem className="woocommerce-marketing-campaigns-card__campaign-title">
<Link href={ el.manageUrl }>
<Link
type={
isWCAdmin(
el.manageUrl
)
? 'wc-admin'
: 'external'
}
href={ el.manageUrl }
>
{ el.title }
</Link>
</FlexItem>

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix save button is still disabled after updating logo settings

View File

@ -1,4 +0,0 @@
Significance: patch
Type: fix
Preload Jetpack-related data from the Jetpack Connection package

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Rename the reference to the 'Footer with Simple Menu and Cart' pattern

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Fix marketing campaign link not navigating to the right page.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Add stalebot schedules to allow processing of all issues

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Adds e2e tests for tax display in store, cart and checkout

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Avoid the PHP error with an undefined property on the WooCommerce > Extensions page.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix CYS initial pattern population bug

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix cys ui issues

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix for PR tests and daily tests

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Allow null value in cost field for multichannel campaign.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: performance
use multiple endpoints to improve performance

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fixed warning on wc_get_product_variation_attributes when product does not exist

View File

@ -1263,7 +1263,7 @@ class WC_Admin_Addons {
'title' => $locale->title,
'description' => $locale->description,
'image' => ( 'http' === substr( $locale->image, 0, 4 ) ) ? $locale->image : WC()->plugin_url() . $locale->image,
'image_alt' => $locale->image_alt,
'image_alt' => $locale->image_alt ?? '',
'actions' => $promotion_actions,
);
}

View File

@ -685,7 +685,7 @@ function wc_get_product_id_by_sku( $sku ) {
*/
function wc_get_product_variation_attributes( $variation_id ) {
// Build variation data from meta.
$all_meta = get_post_meta( $variation_id );
$all_meta = is_array( get_post_meta( $variation_id ) ) ? get_post_meta( $variation_id ) : array();
$parent_id = wp_get_post_parent_id( $variation_id );
$parent_attributes = array_filter( (array) get_post_meta( $parent_id, '_product_attributes', true ) );
$found_parent_attributes = array();

View File

@ -224,7 +224,7 @@ module.exports = async ( config ) => {
}
}
!process.env.BASE_URL || process.env.BASE_URL === 'localhost' && await site.useCartCheckoutShortcodes( baseURL, userAgent, admin );
await site.useCartCheckoutShortcodes( baseURL, userAgent, admin );
await adminContext.close();
await customerContext.close();

View File

@ -0,0 +1,695 @@
const { test, expect } = require( '@playwright/test' );
const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default;
const { admin, customer } = require( '../../test-data/data' );
const productName = 'Taxed products are awesome';
const productPrice = '100.00';
const messyProductPrice = '13.47';
const secondProductName = 'Other products are also awesome';
let productId, productId2, nastyTaxId, seventeenTaxId, sixTaxId, countryTaxId, stateTaxId, cityTaxId, zipTaxId, shippingTaxId, shippingZoneId, shippingMethodId;
test.describe( 'Shopper Tax Display Tests', () => {
test.beforeAll( async ( { baseURL } ) => {
const api = new wcApi( {
url: baseURL,
consumerKey: process.env.CONSUMER_KEY,
consumerSecret: process.env.CONSUMER_SECRET,
version: 'wc/v3',
} );
await api.put( 'settings/general/woocommerce_calc_taxes', {
value: 'yes',
} );
await api.post( 'products', {
name: productName,
type: 'simple',
regular_price: productPrice,
} )
.then( ( response ) => {
productId = response.data.id;
} );
await api.post( 'taxes', {
"country": "US",
"state": "*",
"cities": "*",
"postcodes": "*",
"rate": "25",
"name": "Nasty Tax",
"shipping": false
} )
.then( ( response ) => {
nastyTaxId = response.data.id;
} );
} );
test.beforeEach( async ( { page, context } ) => {
// Shopping cart is very sensitive to cookies, so be explicit
await context.clearCookies();
// all tests use the first product
await page.goto( `/shop/?add-to-cart=${ productId }`, { waitUntil: 'networkidle' } );
} );
test.afterAll( async ( { baseURL } ) => {
const api = new wcApi( {
url: baseURL,
consumerKey: process.env.CONSUMER_KEY,
consumerSecret: process.env.CONSUMER_SECRET,
version: 'wc/v3',
} );
await api.put( 'settings/tax/woocommerce_tax_display_cart', {
value: 'excl',
} );
await api.put( 'settings/tax/woocommerce_tax_display_shop', {
value: 'excl'
} );
await api.put( 'settings/tax/woocommerce_price_display_suffix', {
value: '',
} );
await api.put( 'settings/general/woocommerce_calc_taxes', {
value: 'no',
} );
await api.put( 'settings/tax/woocommerce_tax_total_display', {
value: 'itemized',
} );
await api.delete( `products/${ productId }`, {
force: true,
} );
await api.delete( `taxes/${ nastyTaxId }`, {
force: true,
} );
} );
test( 'checks that taxes are calculated properly on totals, inclusive tax displayed properly', async ( { page, baseURL } ) => {
const api = new wcApi( {
url: baseURL,
consumerKey: process.env.CONSUMER_KEY,
consumerSecret: process.env.CONSUMER_SECRET,
version: 'wc/v3',
} );
await api.put( 'settings/tax/woocommerce_tax_display_cart', {
value: 'incl',
} );
await api.put( 'settings/tax/woocommerce_tax_display_shop', {
value: 'incl'
} );
await test.step( 'Load shop page and confirm price display', async() => {
await page.goto( '/shop/' );
await expect( page.getByRole( 'heading', { name: 'Shop' } ) ).toBeVisible();
await expect( page.getByRole('link', { name: 'Placeholder Taxed products are awesome $125.00' }).first() ).toBeVisible();
} );
await test.step( 'Load cart page and confirm price display', async() => {
await page.goto( '/cart/' );
await expect( page.getByRole( 'heading', { name: 'Cart', exact: true } ) ).toBeVisible();
await expect( page.getByRole( 'cell', { name: '$125.00 (incl. tax)' } ) ).toHaveCount(2);
await expect( page.getByRole( 'row', { name: 'Subtotal $125.00 (incl. tax)'} ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Total $125.00 (includes $25.00 Nasty Tax)' } ) ).toBeVisible();
} );
await test.step( 'Load checkout page and confirm price display', async() => {
await page.goto( '/checkout/' );
await expect( page.getByRole( 'heading', { name: 'Checkout' } ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Taxed products are awesome × 1 $125.00 (incl. tax)' } ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Subtotal $125.00 (incl. tax)' } ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Total $125.00 (includes $25.00 Nasty Tax)'} ) ).toBeVisible();
} );
} );
test( 'checks that taxes are calculated and displayed correctly exclusive on shop, cart and checkout', async ( { page, baseURL } ) => {
const api = new wcApi( {
url: baseURL,
consumerKey: process.env.CONSUMER_KEY,
consumerSecret: process.env.CONSUMER_SECRET,
version: 'wc/v3',
} );
await api.put( 'settings/tax/woocommerce_tax_display_cart', {
value: 'excl',
} );
await api.put( 'settings/tax/woocommerce_tax_display_shop', {
value: 'excl'
} );
await test.step( 'Load shop page and confirm price display', async() => {
await page.goto( '/shop/' );
await expect( page.getByRole( 'heading', { name: 'Shop' } ) ).toBeVisible();
await expect( page.getByRole('link', { name: 'Placeholder Taxed products are awesome $100.00' }).first() ).toBeVisible();
} );
await test.step( 'Load cart page and confirm price display', async() => {
await page.goto( '/cart/' );
await expect( page.getByRole( 'heading', { name: 'Cart', exact: true } ) ).toBeVisible();
await expect( page.getByRole( 'cell', { name: '$100.00' } ) ).toHaveCount(3);
await expect( page.getByRole( 'row', { name: 'Subtotal $100.00'} ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Tax $25.00' } ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Total $125.00' } ) ).toBeVisible();
} );
await test.step( 'Load checkout page and confirm price display', async() => {
await page.goto( '/checkout/' );
await expect( page.getByRole( 'heading', { name: 'Checkout' } ) ).toBeVisible();
await page.locator( '#billing_first_name' ).fill( customer.billing.us.first_name );
await page.locator( '#billing_last_name' ).fill( customer.billing.us.last_name );
await page.locator( '#billing_address_1' ).fill( customer.billing.us.address );
await page.locator( '#billing_city' ).fill( customer.billing.us.city );
await page.locator( '#billing_country' ).selectOption( customer.billing.us.country );
await page.locator( '#billing_state' ).selectOption( customer.billing.us.state );
await page.locator( '#billing_postcode' ).fill( customer.billing.us.zip );
await page.locator( '#billing_phone' ).fill( customer.billing.us.phone );
await page.locator( '#billing_email' ).fill( customer.billing.us.email );
await expect( page.getByRole( 'row', { name: 'Taxed products are awesome × 1 $100.00' } ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Subtotal $100.00' } ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Tax $25.00' } ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Total $125.00' } ) ).toBeVisible();
} );
} );
test( 'checks that display suffix is shown', async ( { page, baseURL } ) => {
const api = new wcApi( {
url: baseURL,
consumerKey: process.env.CONSUMER_KEY,
consumerSecret: process.env.CONSUMER_SECRET,
version: 'wc/v3',
} );
await api.put( 'settings/tax/woocommerce_tax_display_cart', {
value: 'excl',
} );
await api.put( 'settings/tax/woocommerce_tax_display_shop', {
value: 'excl',
} );
await api.put( 'settings/tax/woocommerce_price_display_suffix', {
value: 'excluding VAT',
} );
await test.step( 'Load shop page and confirm price suffix display', async() => {
await page.goto( '/shop/' );
await expect( page.getByRole( 'heading', { name: 'Shop' } ) ).toBeVisible();
await expect( page.getByRole('link', { name: 'Placeholder Taxed products are awesome $100.00 excluding VAT' }).first() ).toBeVisible();
} );
} );
} );
test.describe( 'Shopper Tax Rounding', () => {
test.beforeAll( async ( { baseURL } ) => {
const api = new wcApi( {
url: baseURL,
consumerKey: process.env.CONSUMER_KEY,
consumerSecret: process.env.CONSUMER_SECRET,
version: 'wc/v3',
} );
await api.put( 'settings/general/woocommerce_calc_taxes', {
value: 'yes',
} );
await api.post( 'products', {
name: productName,
type: 'simple',
regular_price: messyProductPrice,
} )
.then( ( response ) => {
productId = response.data.id;
} );
await api.post( 'products', {
name: secondProductName,
type: 'simple',
regular_price: messyProductPrice,
} )
.then( ( response ) => {
productId2 = response.data.id;
} );
await api.post( 'taxes', {
"country": "US",
"state": "*",
"cities": "*",
"postcodes": "*",
"rate": "17",
"name": "Seventeen Tax",
"shipping": false,
"compound": true,
"priority": 1
} )
.then( ( response ) => {
seventeenTaxId = response.data.id;
} );
await api.post( 'taxes', {
"country": "US",
"state": "*",
"cities": "*",
"postcodes": "*",
"rate": "6",
"name": "Six Tax",
"shipping": false,
"compound": true,
"priority": 2
} )
.then( ( response ) => {
sixTaxId = response.data.id;
} );
} );
test.beforeEach( async ( { page, context } ) => {
// Shopping cart is very sensitive to cookies, so be explicit
await context.clearCookies();
// all tests use the first product
await page.goto( `/shop/?add-to-cart=${ productId }`, { waitUntil: 'networkidle' } );
await page.goto( `/shop/?add-to-cart=${ productId2 }`, { waitUntil: 'networkidle' } );
await page.goto( `/shop/?add-to-cart=${ productId2 }`, { waitUntil: 'networkidle' } );
} );
test.afterAll( async ( { baseURL } ) => {
const api = new wcApi( {
url: baseURL,
consumerKey: process.env.CONSUMER_KEY,
consumerSecret: process.env.CONSUMER_SECRET,
version: 'wc/v3',
} );
await api.put( 'settings/tax/woocommerce_tax_display_cart', {
value: 'excl',
} );
await api.put( 'settings/tax/woocommerce_tax_display_shop', {
value: 'excl'
} );
await api.put( 'settings/tax/woocommerce_tax_round_at_subtotal', {
value: 'no',
} );
await api.put( 'settings/general/woocommerce_calc_taxes', {
value: 'no',
} );
await api.put( 'settings/tax/woocommerce_tax_total_display', {
value: 'itemized'
} );
await api.delete( `products/${ productId }`, {
force: true,
} );
await api.delete( `products/${ productId2 }`, {
force: true,
} );
await api.delete( `taxes/${ seventeenTaxId }`, {
force: true,
} );
await api.delete( `taxes/${ sixTaxId }`, {
force: true,
} );
} );
test( 'checks rounding at subtotal level', async ( { page, baseURL } ) => {
const api = new wcApi( {
url: baseURL,
consumerKey: process.env.CONSUMER_KEY,
consumerSecret: process.env.CONSUMER_SECRET,
version: 'wc/v3',
} );
await api.put( 'settings/tax/woocommerce_tax_display_cart', {
value: 'excl',
} );
await api.put( 'settings/tax/woocommerce_tax_display_shop', {
value: 'excl',
} );
await api.put( 'settings/tax/woocommerce_tax_round_at_subtotal', {
value: 'yes',
} );
await api.put( 'settings/tax/woocommerce_tax_total_display', {
value: 'single',
} );
await test.step( 'Load shop page and confirm price display', async() => {
await page.goto( '/shop/' );
await expect( page.getByRole( 'heading', { name: 'Shop' } ) ).toBeVisible();
await expect( page.getByRole('link', { name: 'Placeholder Taxed products are awesome $13.47' }).first() ).toBeVisible();
} );
await test.step( 'Load cart page and confirm price display', async() => {
await page.goto( '/cart/' );
await expect( page.getByRole( 'heading', { name: 'Cart', exact: true } ) ).toBeVisible();
await expect( page.getByRole( 'cell', { name: '$13.47' } ) ).toHaveCount(3);
await expect( page.getByRole( 'row', { name: 'Subtotal $40.41'} ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Tax $9.71 ' } ) ).toBeVisible()
await expect( page.getByRole( 'row', { name: 'Total $50.12 ' } ) ).toBeVisible();
} );
} );
test( 'checks rounding off at subtotal level', async ( { page, baseURL } ) => {
const api = new wcApi( {
url: baseURL,
consumerKey: process.env.CONSUMER_KEY,
consumerSecret: process.env.CONSUMER_SECRET,
version: 'wc/v3',
} );
await api.put( 'settings/tax/woocommerce_tax_display_cart', {
value: 'excl',
} );
await api.put( 'settings/tax/woocommerce_tax_display_shop', {
value: 'excl',
} );
await api.put( 'settings/tax/woocommerce_tax_round_at_subtotal', {
value: 'no',
} );
await api.put( 'settings/tax/woocommerce_tax_total_display', {
value: 'itemized',
} );
await test.step( 'Load shop page and confirm price display', async() => {
await page.goto( '/shop/' );
await expect( page.getByRole( 'heading', { name: 'Shop' } ) ).toBeVisible();
await expect( page.getByRole('link', { name: 'Placeholder Taxed products are awesome $13.47' }).first() ).toBeVisible();
} );
await test.step( 'Load cart page and confirm price display', async() => {
await page.goto( '/cart/' );
await expect( page.getByRole( 'heading', { name: 'Cart', exact: true } ) ).toBeVisible();
await expect( page.getByRole( 'cell', { name: '$13.47' } ) ).toHaveCount(3);
await expect( page.getByRole( 'row', { name: 'Subtotal $40.41'} ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Seventeen Tax $6.87 ' } ) ).toBeVisible()
await expect( page.getByRole( 'row', { name: 'Six Tax $2.84 ' } ) ).toBeVisible()
await expect( page.getByRole( 'row', { name: 'Total $50.12 ' } ) ).toBeVisible();
} );
} );
} );
test.describe( 'Shopper Tax Levels', () => {
test.beforeAll( async ( { baseURL } ) => {
const api = new wcApi( {
url: baseURL,
consumerKey: process.env.CONSUMER_KEY,
consumerSecret: process.env.CONSUMER_SECRET,
version: 'wc/v3',
} );
await api.put( 'settings/general/woocommerce_calc_taxes', {
value: 'yes',
} );
await api.put( 'settings/tax/woocommerce_tax_display_cart', {
value: 'excl',
} );
await api.post( 'products', {
name: productName,
type: 'simple',
regular_price: productPrice,
} )
.then( ( response ) => {
productId = response.data.id;
} );
await api.post( 'taxes', {
"country": "US",
"state": "*",
"cities": "*",
"postcodes": "*",
"rate": "10",
"name": "Country Tax",
"shipping": false,
"priority": 1
} )
.then( ( response ) => {
countryTaxId = response.data.id;
} );
await api.post( 'taxes', {
"country": "*",
"state": "CA",
"cities": "*",
"postcodes": "*",
"rate": "5",
"name": "State Tax",
"shipping": false,
"priority": 2
} )
.then( ( response ) => {
stateTaxId = response.data.id;
} );
await api.post( 'taxes', {
"country": "*",
"state": "*",
"cities": "Sacramento",
"postcodes": "*",
"rate": "2.5",
"name": "City Tax",
"shipping": false,
"priority": 3
} )
.then( ( response ) => {
cityTaxId = response.data.id;
} );
await api.post( 'taxes', {
"country": "*",
"state": "*",
"cities": "*",
"postcodes": "55555",
"rate": "1.25",
"name": "Zip Tax",
"shipping": false,
"priority": 4
} )
.then( ( response ) => {
zipTaxId = response.data.id;
} );
} );
test.beforeEach( async ( { page, context } ) => {
// Shopping cart is very sensitive to cookies, so be explicit
await context.clearCookies();
// all tests use the first product
await page.goto( `/shop/?add-to-cart=${ productId }`, { waitUntil: 'networkidle' } );
} );
test.afterAll( async ( { baseURL } ) => {
const api = new wcApi( {
url: baseURL,
consumerKey: process.env.CONSUMER_KEY,
consumerSecret: process.env.CONSUMER_SECRET,
version: 'wc/v3',
} );
await api.put( 'settings/tax/woocommerce_tax_total_display', {
value: 'itemized'
} );
await api.put( 'settings/tax/woocommerce_tax_display_cart', {
value: 'excl',
} );
await api.delete( `products/${ productId }`, {
force: true,
} );
await api.delete( `taxes/${ countryTaxId }`, {
force: true,
} );
await api.delete( `taxes/${ stateTaxId }`, {
force: true,
} );
await api.delete( `taxes/${ cityTaxId }`, {
force: true,
} );
await api.delete( `taxes/${ zipTaxId }`, {
force: true,
} );
} );
test( 'checks applying taxes of 4 different levels', async ( { page, baseURL } ) => {
const api = new wcApi( {
url: baseURL,
consumerKey: process.env.CONSUMER_KEY,
consumerSecret: process.env.CONSUMER_SECRET,
version: 'wc/v3',
} );
await api.put( 'settings/tax/woocommerce_tax_total_display', {
value: 'itemized',
} );
await test.step( 'Load cart page and confirm price display', async() => {
await page.goto( '/cart/' );
await expect( page.getByRole( 'heading', { name: 'Cart', exact: true } ) ).toBeVisible();
await expect( page.getByRole( 'cell', { name: '$100.00' } ) ).toHaveCount(3);
await expect( page.getByRole( 'row', { name: 'Subtotal $100.00'} ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Country Tax $10.00 ' } ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'State Tax $5.00 ' } ) ).toBeVisible()
await expect( page.getByRole( 'row', { name: 'Total $115.00 ' } ) ).toBeVisible();
} );
await test.step( 'Load checkout page and confirm taxes displayed', async() => {
await page.goto( '/checkout/' );
await expect( page.getByRole( 'heading', { name: 'Checkout', exact: true } ) ).toBeVisible();
await page.getByLabel('First name *').first().fill( customer.billing.us.first_name );
await page.getByLabel('Last name *').first().fill( customer.billing.us.last_name );
await page.getByPlaceholder('House number and street name').first().fill( customer.billing.us.address );
await page.getByLabel('Town / City *').first().pressSequentially( 'Sacramento' );
await page.getByLabel('ZIP Code *').first().pressSequentially( '55555' );
await page.getByLabel('Phone *').first().fill( customer.billing.us.phone );
await page.getByLabel('Email address *').first().fill( customer.billing.us.email );
await expect( page.getByRole( 'row', { name: 'Subtotal $100.00'} ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Country Tax $10.00' } ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'State Tax $5.00' } ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'City Tax $2.50' } ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Zip Tax $1.25' } ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Total $118.75 ' } ) ).toBeVisible();
} );
} );
test( 'checks applying taxes of 2 different levels (2 excluded)', async ( { page, baseURL } ) => {
const api = new wcApi( {
url: baseURL,
consumerKey: process.env.CONSUMER_KEY,
consumerSecret: process.env.CONSUMER_SECRET,
version: 'wc/v3',
} );
await api.put( 'settings/tax/woocommerce_tax_total_display', {
value: 'itemized',
} );
await test.step( 'Load cart page and confirm price display', async() => {
await page.goto( '/cart/' );
await expect( page.getByRole( 'heading', { name: 'Cart', exact: true } ) ).toBeVisible();
await expect( page.getByRole( 'cell', { name: '$100.00' } ) ).toHaveCount(3);
await expect( page.getByRole( 'row', { name: 'Subtotal $100.00'} ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Country Tax $10.00 ' } ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'State Tax $5.00 ' } ) ).toBeVisible()
await expect( page.getByRole( 'row', { name: 'Total $115.00 ' } ) ).toBeVisible();
} );
await test.step( 'Load checkout page and confirm taxes displayed', async() => {
await page.goto( '/checkout/' );
await expect( page.getByRole( 'heading', { name: 'Checkout', exact: true } ) ).toBeVisible();
await page.getByLabel('First name *').first().fill( customer.billing.us.first_name );
await page.getByLabel('Last name *').first().fill( customer.billing.us.last_name );
await page.getByPlaceholder('House number and street name').first().fill( customer.billing.us.address );
await page.getByLabel('Town / City *').first().pressSequentially( customer.billing.us.city );
await page.getByLabel('ZIP Code *').first().pressSequentially( customer.billing.us.zip );
await page.getByLabel('Phone *').first().fill( customer.billing.us.phone );
await page.getByLabel('Email address *').first().fill( customer.billing.us.email );
await expect( page.getByRole( 'row', { name: 'Subtotal $100.00'} ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Country Tax $10.00' } ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'State Tax $5.00' } ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'City Tax $2.50' } ) ).not.toBeVisible();
await expect( page.getByRole( 'row', { name: 'Zip Tax $1.25' } ) ).not.toBeVisible();
await expect( page.getByRole( 'row', { name: 'Total $115.00 ' } ) ).toBeVisible();
} );
} );
} );
test.describe( 'Shipping Tax', () => {
test.beforeAll( async ( { baseURL } ) => {
const api = new wcApi( {
url: baseURL,
consumerKey: process.env.CONSUMER_KEY,
consumerSecret: process.env.CONSUMER_SECRET,
version: 'wc/v3',
} );
await api.put( 'settings/general/woocommerce_calc_taxes', {
value: 'yes',
} );
await api.post( 'products', {
name: productName,
type: 'simple',
regular_price: productPrice,
} )
.then( ( response ) => {
productId = response.data.id;
} );
await api.post( 'taxes', {
"country": "US",
"state": "*",
"cities": "*",
"postcodes": "*",
"rate": "15",
"name": "Shipping Tax",
"shipping": true
} )
.then( ( response ) => {
shippingTaxId = response.data.id;
} );
await api.post( 'shipping/zones', {
name: 'All',
} )
.then( ( response ) => {
shippingZoneId = response.data.id;
} );
await api.post( `shipping/zones/${ shippingZoneId }/methods`, {
method_id: 'flat_rate',
} )
.then( ( response ) => {
shippingMethodId = response.data.id;
} );
await api.put( `shipping/zones/${ shippingZoneId }/methods/${ shippingMethodId }`, {
settings: {
cost: '20.00',
}
} );
await api.put( 'payment_gateways/cod' , {
enabled: true
} );
await api.put( 'settings/tax/woocommerce_tax_display_cart', {
value: 'incl',
} );
} );
test.beforeEach( async ( { page, context } ) => {
// Shopping cart is very sensitive to cookies, so be explicit
await context.clearCookies();
// all tests use the first product
await page.goto( `/shop/?add-to-cart=${ productId }`, { waitUntil: 'networkidle' } );
} );
test.afterAll( async ( { baseURL } ) => {
const api = new wcApi( {
url: baseURL,
consumerKey: process.env.CONSUMER_KEY,
consumerSecret: process.env.CONSUMER_SECRET,
version: 'wc/v3',
} );
await api.put( 'settings/general/woocommerce_calc_taxes', {
value: 'no',
} );
await api.delete( `products/${ productId }`, {
force: true,
} );
await api.delete( `taxes/${ shippingTaxId }`, {
force: true,
} );
await api.put( 'payment_gateways/cod' , {
enabled: false
} );
await api.delete( `shipping/zones/${ shippingZoneId }`, {
force: true,
} );
} );
test( 'checks that tax is applied to shipping as well as order', async ( { page, baseURL } ) => {
await test.step( 'Load cart page and confirm price display', async() => {
await page.goto( '/cart/' );
await expect( page.getByRole( 'heading', { name: 'Cart', exact: true } ) ).toBeVisible();
await expect( page.getByRole( 'cell', { name: '$115.00 (incl. tax)' } ) ).toHaveCount(2);
await expect( page.getByRole( 'row', { name: 'Subtotal $115.00 (incl. tax)'} ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Shipping Flat rate: $23.00 (incl. tax) Shipping to CA.' } ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Total $138.00 (includes $18.00 Shipping Tax)' } ) ).toBeVisible();
} );
await test.step( 'Load checkout page and confirm price display', async() => {
await page.goto( '/checkout/' );
await expect( page.getByRole( 'heading', { name: 'Checkout' } ) ).toBeVisible();
await page.getByRole('textbox', { name: 'First name *' }).fill( customer.billing.us.first_name );
await page.getByRole('textbox', { name: 'Last name *' }).fill( customer.billing.us.last_name );
await page.getByRole('textbox', { name: 'Street address *' }).fill( customer.billing.us.address );
await page.getByRole('textbox', { name: 'Town / City *' }).type( customer.billing.us.city );
await page.getByRole('textbox', { name: 'ZIP Code *' }).type( customer.billing.us.zip );
await page.getByLabel('Phone *').fill( customer.billing.us.phone );
await page.getByLabel('Email address *').fill( customer.billing.us.email );
await expect( page.getByRole( 'row', { name: 'Taxed products are awesome × 1 $115.00 (incl. tax)' } ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Subtotal $115.00 (incl. tax)' } ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Shipping Flat rate: $23.00 (incl. tax)' } ) ).toBeVisible();
await expect( page.getByRole( 'row', { name: 'Total $138.00 (includes $18.00 Shipping Tax)'} ) ).toBeVisible();
} );
} );
} );