diff --git a/.github/actions/e2e-pw/run-log-tests/action.yml b/.github/actions/e2e-pw/run-log-tests/action.yml new file mode 100644 index 00000000000..d8f85f00285 --- /dev/null +++ b/.github/actions/e2e-pw/run-log-tests/action.yml @@ -0,0 +1,17 @@ +name: 'Run Tests' +description: 'Runs Playwright E2E tests' + +runs: + using: 'composite' + steps: + - name: First Run Playwright E2E Tests + id: first_run_e2e_pw_tests + # Use +e to trap errors when running E2E tests. + shell: /bin/bash +e {0} + run: npm run test:e2e-pw-ci + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 diff --git a/.github/workflows/e2e-pw-pull-request.yml b/.github/workflows/e2e-pw-pull-request.yml new file mode 100644 index 00000000000..0cc22f767c2 --- /dev/null +++ b/.github/workflows/e2e-pw-pull-request.yml @@ -0,0 +1,74 @@ +name: E2E Playwright Tests - Pull Request + +on: + pull_request: + branches: + - develop + - trunk + workflow_dispatch: + workflow_call: + inputs: + wcpay-use-build-artifact: + type: boolean + required: false + default: false + repo-branch: + type: string + required: false + description: 'Branch to be used for running tests' + +env: + E2E_GH_TOKEN: ${{ secrets.E2E_GH_TOKEN }} + WCP_DEV_TOOLS_REPO: ${{ secrets.WCP_DEV_TOOLS_REPO }} + WCP_DEV_TOOLS_BRANCH: 'trunk' + WCP_SERVER_REPO: ${{ secrets.WCP_SERVER_REPO }} + WC_SUBSCRIPTIONS_REPO: ${{ secrets.WC_SUBSCRIPTIONS_REPO }} + E2E_BLOG_ID: ${{ secrets.E2E_BLOG_ID }} + E2E_BLOG_TOKEN: ${{ secrets.E2E_BLOG_TOKEN }} + E2E_USER_TOKEN: ${{ secrets.E2E_USER_TOKEN }} + WC_E2E_SCREENSHOTS: 1 + E2E_SLACK_CHANNEL: ${{ secrets.E2E_SLACK_CHANNEL }} + E2E_SLACK_TOKEN: ${{ secrets.E2E_SLACK_TOKEN }} + E2E_USE_LOCAL_SERVER: false + E2E_RESULT_FILEPATH: 'tests/e2e/results.json' + WCPAY_USE_BUILD_ARTIFACT: ${{ inputs.wcpay-use-build-artifact }} + WCPAY_ARTIFACT_DIRECTORY: 'zipfile' + NODE_ENV: 'test' + FORCE_E2E_DEPS_SETUP: true + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + wcpay-e2e-tests: + runs-on: ubuntu-latest + + name: WC - latest + + env: + E2E_WP_VERSION: 'latest' + E2E_WC_VERSION: 'latest' + + steps: + - name: Checkout WCPay repository + uses: actions/checkout@v4 + with: + ref: ${{ inputs.repo-branch || github.ref }} + + - name: 'Download WooCommerce Payments build file' + if: ${{ inputs.wcpay-use-build-artifact }} + uses: actions/download-artifact@v4 + with: + name: woocommerce-payments + path: ${{ env.WCPAY_ARTIFACT_DIRECTORY }} + + - name: Setup E2E environment + uses: ./.github/actions/e2e/env-setup + + - name: Install Playwright + shell: bash + run: npx playwright install chromium + + - name: Run tests, upload screenshots & logs + uses: ./.github/actions/e2e-pw/run-log-tests diff --git a/.gitignore b/.gitignore index 4aabb410301..a0125db32a6 100644 --- a/.gitignore +++ b/.gitignore @@ -87,5 +87,12 @@ tests/e2e/screenshots # E2E Performance test results tests/e2e/reports +# E2E Playwright +/playwright-report/ +/blob-report/ +tests/e2e-pw/.auth/ +tests/e2e-pw/test-results/ +tests/e2e-pw/playwright/.cache/ +tests/e2e-pw/tests/e2e-pw/.auth/* # Slate docs docs/rest-api/build/* diff --git a/changelog/add-8197-overview-survey b/changelog/add-8197-overview-survey new file mode 100644 index 00000000000..48296ce1735 --- /dev/null +++ b/changelog/add-8197-overview-survey @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add User Satisfaction Survey for Payments Overview Widget diff --git a/changelog/add-8511-add-payment-activity-data-tooltip b/changelog/add-8511-add-payment-activity-data-tooltip new file mode 100644 index 00000000000..ba2fba7174d --- /dev/null +++ b/changelog/add-8511-add-payment-activity-data-tooltip @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Non user-facing changes. Behind feature flag. Add tooltip messages to tiles within Payment activity widget diff --git a/changelog/add-monitoring-related-headers b/changelog/add-monitoring-related-headers new file mode 100644 index 00000000000..6bf0c138c16 --- /dev/null +++ b/changelog/add-monitoring-related-headers @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: We are adding a new header to REST backend requests in order to get insights into requests latency. + + diff --git a/changelog/fix-8672-remove-obsolete-docker-compose-version b/changelog/fix-8672-remove-obsolete-docker-compose-version new file mode 100644 index 00000000000..3823053ecde --- /dev/null +++ b/changelog/fix-8672-remove-obsolete-docker-compose-version @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Remove obsolete docker-compose key `version` diff --git a/changelog/remove-deprecated-param-from-asset-data-registry b/changelog/remove-deprecated-param-from-asset-data-registry new file mode 100644 index 00000000000..a18738ea4b8 --- /dev/null +++ b/changelog/remove-deprecated-param-from-asset-data-registry @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Remove deprecated param from asset data registry interface. diff --git a/changelog/try-playwright-e2e-tests b/changelog/try-playwright-e2e-tests new file mode 100644 index 00000000000..e903ebf8ee9 --- /dev/null +++ b/changelog/try-playwright-e2e-tests @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Add Playwright e2e test suite ready for incremental migration and visual regression testing diff --git a/client/components/payment-activity/index.tsx b/client/components/payment-activity/index.tsx index 24cbfac77d4..4f60bf3b573 100644 --- a/client/components/payment-activity/index.tsx +++ b/client/components/payment-activity/index.tsx @@ -4,19 +4,23 @@ import * as React from 'react'; import { Card, CardBody, CardHeader } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import interpolateComponents from '@automattic/interpolate-components'; /** * Internal dependencies */ import EmptyStateAsset from 'assets/images/payment-activity-empty-state.svg?asset'; -import interpolateComponents from '@automattic/interpolate-components'; import PaymentActivityData from './payment-activity-data'; +import Survey from './survey'; +import { WcPayOverviewSurveyContextProvider } from './survey/context'; import './style.scss'; const PaymentActivity: React.FC = () => { const { lifetimeTPV } = wcpaySettings; const hasAtLeastOnePayment = lifetimeTPV > 0; + const isOverviewSurveySubmitted = + wcpaySettings.isOverviewSurveySubmitted ?? false; return ( @@ -50,6 +54,12 @@ const PaymentActivity: React.FC = () => { ) } + + { ! isOverviewSurveySubmitted && hasAtLeastOnePayment && ( + + + + ) } ); }; diff --git a/client/components/payment-activity/payment-activity-data.tsx b/client/components/payment-activity/payment-activity-data.tsx index e8f1641f2a8..9ec3b56974c 100644 --- a/client/components/payment-activity/payment-activity-data.tsx +++ b/client/components/payment-activity/payment-activity-data.tsx @@ -5,16 +5,17 @@ import * as React from 'react'; import moment from 'moment'; import { __ } from '@wordpress/i18n'; import HelpOutlineIcon from 'gridicons/dist/help-outline'; +import interpolateComponents from '@automattic/interpolate-components'; /** * Internal dependencies. */ +import InlineNotice from '../inline-notice'; import PaymentDataTile from './payment-data-tile'; import { ClickTooltip } from '../tooltip'; import { usePaymentActivityData } from 'wcpay/data'; import { getAdminUrl } from 'wcpay/utils'; import type { DateRange } from './types'; - import './style.scss'; /** @@ -50,16 +51,35 @@ const PaymentActivityData: React.FC = () => { currencyCode={ storeCurrency } tooltip={ } buttonLabel={ __( 'Total payment volume tooltip', 'woocommerce-payments' ) } - content={ __( - 'test total payment volume content', - 'woocommerce-payments' - ) } + content={ + <> + { interpolateComponents( { + mixedString: __( + '{{strong}}Total payment volume{{/strong}} is gross value of payments successfully processed over a given timeframe.', + 'woocommerce-payments' + ), + components: { + strong: , + }, + } ) } + + { __( + 'Total payment volume = Charges - Refunds - Disputes', + 'woocommerce-payments' + ) } + + + } /> } amount={ totalPaymentVolume } @@ -89,7 +109,15 @@ const PaymentActivityData: React.FC = () => { 'Charges tooltip', 'woocommerce-payments' ) } - content={ __( 'test charge content' ) } + content={ interpolateComponents( { + mixedString: __( + 'A {{strong}}charge{{/strong}} is the amount billed to your customer’s payment method.', + 'woocommerce-payments' + ), + components: { + strong: , + }, + } ) } /> } amount={ charges } @@ -143,10 +171,15 @@ const PaymentActivityData: React.FC = () => { 'Fees tooltip', 'woocommerce-payments' ) } - content={ __( - 'test fees content', - 'woocommerce-payments' - ) } + content={ interpolateComponents( { + mixedString: __( + '{{strong}}Fees{{/strong}} includes fees on payments as well as disputes.', + 'woocommerce-payments' + ), + components: { + strong: , + }, + } ) } /> } amount={ fees } diff --git a/client/components/payment-activity/style.scss b/client/components/payment-activity/style.scss index acf8dd5a15c..ab75d9370eb 100644 --- a/client/components/payment-activity/style.scss +++ b/client/components/payment-activity/style.scss @@ -19,7 +19,7 @@ padding: 0 !important; &__empty-state-wrapper { text-align: center; - color: #949494; + color: $gray-600; padding: 16px 0 19px; } } @@ -34,6 +34,17 @@ @include breakpoint( '<660px' ) { grid-template-columns: 1fr; padding: 24px; + + > .wcpay-payment-data-highlights__item:nth-of-type( 1 ) { + // Mobile view of the Total payment volume tile. + border-bottom: 1px solid $gray-200; + padding-top: 0; + padding-bottom: 24px; + + .wcpay-tooltip__tooltip-wrapper { + left: 45px !important; + } + } } .wcpay-payment-data-highlights { @@ -127,9 +138,7 @@ flex-direction: column; } } -} -#wcpay-payment-activity-data { &__total-payment-volume { border-left: none; align-self: stretch; @@ -142,10 +151,23 @@ @include amount-styles; } - @include breakpoint( '<660px' ) { - border-bottom: 1px solid $gray-200; - padding-top: 0; - padding-bottom: 24px; + &__tooltip { + .wcpay-inline-notice.components-notice { + margin-top: 8px; + margin-right: 0; + padding: 8px; + color: $studio-blue-70; + + .components-notice__content { + font-size: 11px; + line-height: 16px; + margin: 0; + } + } } } + + .wcpay-tooltip__tooltip { + line-height: 20px; + } } diff --git a/client/components/payment-activity/survey/context.tsx b/client/components/payment-activity/survey/context.tsx new file mode 100644 index 00000000000..9919044b8e3 --- /dev/null +++ b/client/components/payment-activity/survey/context.tsx @@ -0,0 +1,84 @@ +/** + * External dependencies + */ +import React, { createContext, useState, useCallback, useContext } from 'react'; +import apiFetch from '@wordpress/api-fetch'; +import { useDispatch } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { NAMESPACE } from 'data/constants'; +import type { OverviewSurveyFields } from './types'; + +type ResponseStatus = 'pending' | 'resolved' | 'error'; + +const useContextValue = ( initialState: OverviewSurveyFields = {} ) => { + const [ surveySubmitted, setSurveySubmitted ] = useState( false ); + const [ responseStatus, setResponseStatus ] = useState< ResponseStatus >( + 'resolved' + ); + const [ surveyAnswers, setSurveyAnswers ] = useState( initialState ); + + const { createErrorNotice } = useDispatch( 'core/notices' ); + + const submitSurvey = useCallback( + async ( answers: OverviewSurveyFields ) => { + setResponseStatus( 'pending' ); + try { + await apiFetch( { + path: `${ NAMESPACE }/upe_survey/payments-overview`, + method: 'POST', + data: answers, + } ); + setSurveySubmitted( true ); + setResponseStatus( 'resolved' ); + } catch ( e ) { + setResponseStatus( 'error' ); + setSurveySubmitted( false ); + createErrorNotice( + __( + 'An error occurred while submitting the survey. Please try again.', + 'woocommerce-payments' + ) + ); + } + }, + [ setResponseStatus, setSurveySubmitted, createErrorNotice ] + ); + + return { + setSurveySubmitted: submitSurvey, + responseStatus, + surveySubmitted, + surveyAnswers, + setSurveyAnswers, + }; +}; + +type ContextValue = ReturnType< typeof useContextValue >; + +const WcPayOverviewSurveyContext = createContext< ContextValue | null >( null ); + +export const WcPayOverviewSurveyContextProvider: React.FC< { + initialData?: OverviewSurveyFields; +} > = ( { children, initialData } ) => { + return ( + + { children } + + ); +}; + +export const useOverviewSurveyContext = (): ContextValue => { + const context = useContext( WcPayOverviewSurveyContext ); + if ( ! context ) { + throw new Error( 'An error occurred when using survey context' ); + } + return context; +}; + +export default WcPayOverviewSurveyContext; diff --git a/client/components/payment-activity/survey/emoticon.tsx b/client/components/payment-activity/survey/emoticon.tsx new file mode 100644 index 00000000000..67cf4d43a84 --- /dev/null +++ b/client/components/payment-activity/survey/emoticon.tsx @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +import React from 'react'; +import classNames from 'classnames'; + +/** + * Internal dependencies + */ +import type { Rating } from './types'; + +const emoticons: Record< Rating, React.ReactElement > = { + 'very-unhappy': <>😞, + unhappy: <>🫤, + neutral: <>😑, + happy: <>🙂, + 'very-happy': <>😍, +}; + +interface Props { + rating: Rating; + onClick: ( event: React.MouseEvent< HTMLButtonElement > ) => void; + disabled: boolean; + isSelected: boolean; +} + +const Emoticon: React.FC< Props > = ( { + rating, + onClick, + disabled, + isSelected, +} ) => { + return ( + + ); +}; + +export default Emoticon; diff --git a/client/components/payment-activity/survey/index.tsx b/client/components/payment-activity/survey/index.tsx new file mode 100644 index 00000000000..0067159ff18 --- /dev/null +++ b/client/components/payment-activity/survey/index.tsx @@ -0,0 +1,199 @@ +/** @format **/ + +/** + * External dependencies + */ +import React from 'react'; +import { HorizontalRule } from '@wordpress/primitives'; +import { Button, CardFooter, TextareaControl } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { createInterpolateElement, useState } from '@wordpress/element'; +import { Icon, closeSmall } from '@wordpress/icons'; + +/** + * Internal dependencies. + */ +import type { Rating } from './types'; +import { useOverviewSurveyContext } from './context'; +import Emoticon from './emoticon'; +import './style.scss'; + +const Survey: React.FC = () => { + const { + responseStatus, + surveySubmitted, + surveyAnswers, + setSurveyAnswers, + setSurveySubmitted, + } = useOverviewSurveyContext(); + + const [ showComponent, setShowComponent ] = useState( true ); + + const currentRating = surveyAnswers.rating; + const ratingWithComment: Rating[] = [ + 'very-unhappy', + 'unhappy', + 'neutral', + ]; + const ratings: Rating[] = [ + 'very-unhappy', + 'unhappy', + 'neutral', + 'happy', + 'very-happy', + ]; + const showComment = + currentRating && ratingWithComment.includes( currentRating ); + const disableForm = 'pending' === responseStatus; + + const setReviewRating = function ( value?: Rating ) { + const answers = { + ...surveyAnswers, + rating: value, + }; + setSurveyAnswers( answers ); + + // If the user selects a rating that does not require a comment, submit the survey immediately. + if ( value && ! ratingWithComment.includes( value ) ) { + setSurveySubmitted( answers ); + } + }; + + if ( ! showComponent ) { + return null; + } + + if ( surveySubmitted ) { + return ( + +
+
+ + πŸ™Œ + + { __( + 'We appreciate your feedback!', + 'woocommerce-payments' + ) } +
+ +
+ +
+
+
+ ); + } + + return ( + +
+
+ { __( + 'Are those metrics helpful?', + 'woocommerce-payments' + ) } + +
+ { ratings.map( ( rating ) => ( + setReviewRating( rating ) } + isSelected={ rating === currentRating } + /> + ) ) } +
+
+ + { showComment && ( + <> +
+ +
+ + + +
+ { + setSurveyAnswers( ( prev ) => ( { + ...prev, + comments: text, + } ) ); + } } + value={ surveyAnswers.comments ?? '' } + readOnly={ disableForm } + /> +

+ { createInterpolateElement( + __( + 'Your feedback will be only be shared with WooCommerce and treated pursuant to our privacy policy.', + 'woocommerce-payments' + ), + { + a: ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + } + ) } +

+
+ +
+ + +
+ + ) } +
+
+ ); +}; +export default Survey; diff --git a/client/components/payment-activity/survey/style.scss b/client/components/payment-activity/survey/style.scss new file mode 100644 index 00000000000..33972d75bf8 --- /dev/null +++ b/client/components/payment-activity/survey/style.scss @@ -0,0 +1,56 @@ +.wcpay-payments-activity__survey { + position: relative; + width: 100%; + + .components-button.has-icon { + min-width: 32px; + height: 32px; + } + + .survey_container { + margin: 0 auto; + display: flex; + gap: 10px; + justify-content: center; + align-items: center; + + span[role='img'] { + line-height: 32px; + font-size: 20px; + } + + &__emoticons { + display: flex; + justify-content: flex-end; + + .selected { + background-color: $wp-blue-0; + } + } + + @include breakpoint( '<660px' ) { + flex-direction: column; + } + } + + .close_container { + position: absolute; + top: 0; + right: 0; + } + + hr { + margin: 16px auto; + } + + .comment_container { + max-width: 500px; + margin: 0 auto; + + &__disclaimer { + font-size: 11px; + color: $wp-gray-40; + font-style: italic; + } + } +} diff --git a/client/components/payment-activity/survey/test/__snapshots__/index.test.tsx.snap b/client/components/payment-activity/survey/test/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000..aacb8aa6f5f --- /dev/null +++ b/client/components/payment-activity/survey/test/__snapshots__/index.test.tsx.snap @@ -0,0 +1,234 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WcPayOverviewSurveyContextProvider test survey initial display 1`] = ` +
+ +
+`; + +exports[`WcPayOverviewSurveyContextProvider test survey with comments textbox 1`] = ` +
+