diff --git a/apps/admin-x-design-system/src/global/Button.tsx b/apps/admin-x-design-system/src/global/Button.tsx index 6a413e71b8..5c91bfeb26 100644 --- a/apps/admin-x-design-system/src/global/Button.tsx +++ b/apps/admin-x-design-system/src/global/Button.tsx @@ -27,9 +27,11 @@ export interface ButtonProps extends Omit, 'label' loadingIndicatorColor?: LoadingIndicatorColor; outlineOnMobile?: boolean; onClick?: (e?:React.MouseEvent) => void; + testId?: string; } const Button: React.FC = ({ + testId, size = 'md', label = '', hideLabel = false, @@ -142,6 +144,7 @@ const Button: React.FC = ({ ; const buttonElement = React.createElement(tag, {className: className, + 'data-testid': testId, disabled: disabled, type: 'button', onClick: onClick, diff --git a/apps/admin-x-design-system/src/global/modal/Modal.tsx b/apps/admin-x-design-system/src/global/modal/Modal.tsx index 9622f7eeb7..af352f423f 100644 --- a/apps/admin-x-design-system/src/global/modal/Modal.tsx +++ b/apps/admin-x-design-system/src/global/modal/Modal.tsx @@ -416,7 +416,7 @@ const Modal: React.FC = ({ (
{title && {title}}
-
) : diff --git a/apps/admin-x-framework/src/test/acceptance.ts b/apps/admin-x-framework/src/test/acceptance.ts index ea7f3cc2d4..e5c255b891 100644 --- a/apps/admin-x-framework/src/test/acceptance.ts +++ b/apps/admin-x-framework/src/test/acceptance.ts @@ -72,8 +72,7 @@ const defaultLabFlags = { outboundLinkTagging: false, announcementBar: false, signupForm: false, - members: false, - adminXOffers: false + members: false }; // Inject defaultLabFlags into responseFixtures.settings and config diff --git a/apps/admin-x-settings/src/components/Sidebar.tsx b/apps/admin-x-settings/src/components/Sidebar.tsx index cec7372770..45c5190902 100644 --- a/apps/admin-x-settings/src/components/Sidebar.tsx +++ b/apps/admin-x-settings/src/components/Sidebar.tsx @@ -35,7 +35,7 @@ const Sidebar: React.FC = () => { const {updateRoute} = useRouting(); const searchInputRef = useRef(null); const {isAnyTextFieldFocused} = useFocusContext(); - const hasOffersLabs = useFeatureFlag('adminXOffers'); + // const hasOffersLabs = useFeatureFlag('adminXOffers'); // Focus in on search field when pressing "/" useEffect(() => { @@ -142,7 +142,7 @@ const Sidebar: React.FC = () => { {hasRecommendations && } - {hasOffersLabs && hasStripeEnabled && } + {hasStripeEnabled && } {hasTipsAndDonations && } diff --git a/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx b/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx index 793ae8be73..bc6d49a6b9 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx @@ -47,10 +47,6 @@ const features = [{ title: 'Tips & donations', description: 'Enables publishers to collect one-time payments', flag: 'tipsAndDonations' -},{ - title: 'AdminX Offers', - description: 'Enables the new offers UI in AdminX settings', - flag: 'adminXOffers' },{ title: 'Filter by email disabled', description: 'Allows filtering members by email disabled', diff --git a/apps/admin-x-settings/src/components/settings/growth/GrowthSettings.tsx b/apps/admin-x-settings/src/components/settings/growth/GrowthSettings.tsx index 7b704ff768..5d0233a75a 100644 --- a/apps/admin-x-settings/src/components/settings/growth/GrowthSettings.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/GrowthSettings.tsx @@ -18,7 +18,7 @@ export const searchKeywords = { const GrowthSettings: React.FC = () => { const hasTipsAndDonations = useFeatureFlag('tipsAndDonations'); const hasRecommendations = useFeatureFlag('recommendations'); - const hasOffersLabs = useFeatureFlag('adminXOffers'); + // const hasOffersLabs = useFeatureFlag('adminXOffers'); const {config, settings} = useGlobalData(); const hasStripeEnabled = checkStripeEnabled(settings || [], config || {}); @@ -26,7 +26,7 @@ const GrowthSettings: React.FC = () => { {hasRecommendations && } - {hasOffersLabs && hasStripeEnabled && } + {hasStripeEnabled && } {hasTipsAndDonations && } ); diff --git a/apps/admin-x-settings/src/components/settings/growth/offers/AddOfferModal.tsx b/apps/admin-x-settings/src/components/settings/growth/offers/AddOfferModal.tsx index fb98b8123e..ce50b43f07 100644 --- a/apps/admin-x-settings/src/components/settings/growth/offers/AddOfferModal.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/offers/AddOfferModal.tsx @@ -1,5 +1,4 @@ import PortalFrame from '../../membership/portal/PortalFrame'; -import useFeatureFlag from '../../../../hooks/useFeatureFlag'; import {Button} from '@tryghost/admin-x-design-system'; import {ErrorMessages, useForm} from '@tryghost/admin-x-framework/hooks'; import {Form, Icon, PreviewModalContent, Select, SelectOption, TextArea, TextField, showToast} from '@tryghost/admin-x-design-system'; @@ -11,7 +10,6 @@ import {useAddOffer} from '@tryghost/admin-x-framework/api/offers'; import {useBrowseOffers} from '@tryghost/admin-x-framework/api/offers'; import {useEffect, useMemo, useState} from 'react'; import {useGlobalData} from '../../../providers/GlobalDataProvider'; -import {useModal} from '@ebay/nice-modal-react'; import {useRouting} from '@tryghost/admin-x-framework/routing'; // we should replace this with a library @@ -211,6 +209,7 @@ const Sidebar: React.FC = ({tierOptions, option.value === overrides.duration)} + testId='duration-select-offers' title='Duration' onSelect={e => handleDurationChange(e?.value || '')} /> @@ -316,9 +317,7 @@ const AddOfferModal = () => { ]; const [href, setHref] = useState(''); - const modal = useModal(); const {updateRoute} = useRouting(); - const hasOffers = useFeatureFlag('adminXOffers'); const {data: {tiers} = {}} = useBrowseTiers(); const activeTiers = getPaidActiveTiers(tiers || []); const tierCadenceOptions = getTiersCadences(activeTiers); @@ -556,13 +555,6 @@ const AddOfferModal = () => { })); }; - useEffect(() => { - if (!hasOffers) { - modal.remove(); - updateRoute(''); - } - }, [hasOffers, modal, updateRoute]); - const cancelAddOffer = () => { if (allOffers.length > 0) { updateRoute('offers/edit'); diff --git a/apps/admin-x-settings/src/components/settings/growth/offers/EditOfferModal.tsx b/apps/admin-x-settings/src/components/settings/growth/offers/EditOfferModal.tsx index c02fee6dd7..376447a5cb 100644 --- a/apps/admin-x-settings/src/components/settings/growth/offers/EditOfferModal.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/offers/EditOfferModal.tsx @@ -1,6 +1,6 @@ -import NiceModal, {useModal} from '@ebay/nice-modal-react'; +import NiceModal from '@ebay/nice-modal-react'; import PortalFrame from '../../membership/portal/PortalFrame'; -import useFeatureFlag from '../../../../hooks/useFeatureFlag'; +// import useFeatureFlag from '../../../../hooks/useFeatureFlag'; import {Button, ConfirmationModal, Form, PreviewModalContent, TextArea, TextField, showToast} from '@tryghost/admin-x-design-system'; import {ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks'; import {Offer, useBrowseOffersById, useEditOffer} from '@tryghost/admin-x-framework/api/offers'; @@ -177,21 +177,12 @@ const Sidebar: React.FC<{ const EditOfferModal: React.FC<{id: string}> = ({id}) => { const {siteData} = useGlobalData(); - const modal = useModal(); const {updateRoute} = useRouting(); const handleError = useHandleError(); - const hasOffers = useFeatureFlag('adminXOffers'); const {mutateAsync: editOffer} = useEditOffer(); const [href, setHref] = useState(''); - useEffect(() => { - if (!hasOffers) { - modal.remove(); - updateRoute(''); - } - }, [hasOffers, modal, updateRoute]); - const {data: {offers: offerById = []} = {}} = useBrowseOffersById(id ? id : ''); const {formState, saveState, updateForm, setFormState, handleSave, validate, errors, clearError, okProps} = useForm({ diff --git a/apps/admin-x-settings/src/components/settings/growth/offers/OfferSuccess.tsx b/apps/admin-x-settings/src/components/settings/growth/offers/OfferSuccess.tsx index 7d16292ec7..507a96dd24 100644 --- a/apps/admin-x-settings/src/components/settings/growth/offers/OfferSuccess.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/offers/OfferSuccess.tsx @@ -103,7 +103,7 @@ const OfferSuccess: React.FC<{id: string}> = ({id}) => {

You can share the link anywhere. In your newsletter, social media, a podcast, or in-person. It all just works.

- +
OR
diff --git a/apps/admin-x-settings/src/components/settings/growth/offers/OffersIndex.tsx b/apps/admin-x-settings/src/components/settings/growth/offers/OffersIndex.tsx index b581bef775..29ec0555f7 100644 --- a/apps/admin-x-settings/src/components/settings/growth/offers/OffersIndex.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/offers/OffersIndex.tsx @@ -1,4 +1,3 @@ -import useFeatureFlag from '../../../../hooks/useFeatureFlag'; import {Button, Tab, TabView} from '@tryghost/admin-x-design-system'; import {ButtonGroup, ButtonProps} from '@tryghost/admin-x-design-system'; import {Modal} from '@tryghost/admin-x-design-system'; @@ -10,11 +9,11 @@ import {currencyToDecimal, getSymbol} from '../../../../utils/currency'; import {getHomepageUrl} from '@tryghost/admin-x-framework/api/site'; import {numberWithCommas} from '../../../../utils/helpers'; import {useBrowseOffers} from '@tryghost/admin-x-framework/api/offers'; -import {useEffect, useState} from 'react'; import {useGlobalData} from '../../../providers/GlobalDataProvider'; import {useModal} from '@ebay/nice-modal-react'; import {useRouting} from '@tryghost/admin-x-framework/routing'; import {useSortingState} from '../../../providers/SettingsAppProvider'; +import {useState} from 'react'; export type OfferType = 'percent' | 'fixed' | 'trial'; @@ -61,7 +60,6 @@ export const getOfferDiscount = (type: string, amount: number, cadence: string, break; }; - // Check if updatedPrice is negative, if so, set it to 0 if (updatedPrice < 0) { updatedPrice = 0; } @@ -94,7 +92,6 @@ export const CopyLinkButton: React.FC<{offerCode: string}> = ({offerCode}) => { export const OffersIndexModal = () => { const modal = useModal(); const {updateRoute} = useRouting(); - const hasOffers = useFeatureFlag('adminXOffers'); const {data: {offers: allOffers = []} = {}} = useBrowseOffers({ searchParams: { limit: 'all' @@ -123,13 +120,6 @@ export const OffersIndexModal = () => { const sortOption = offersSorting?.option || 'date-added'; const sortDirection = offersSorting?.direction || 'desc'; - useEffect(() => { - if (!hasOffers) { - modal.remove(); - updateRoute(''); - } - }, [hasOffers, modal, updateRoute]); - const handleOfferEdit = (id:string) => { // TODO: implement sessionStorage.setItem('editOfferPageSource', 'offersIndex'); @@ -145,7 +135,6 @@ export const OffersIndexModal = () => { case 'redemptions': return multiplier * (offer1.redemption_count - offer2.redemption_count); default: - // 'date-added' or unknown option, use default sorting return multiplier * ((offer1.created_at ? new Date(offer1.created_at).getTime() : 0) - (offer2.created_at ? new Date(offer2.created_at).getTime() : 0)); } }); @@ -164,7 +153,6 @@ export const OffersIndexModal = () => { } {sortedOffers.filter((offer) => { const offerTier = allTiers?.find(tier => tier.id === offer?.tier.id); - //Check to filter out offers with archived offerTier return (selectedTab === 'active' && (offer.status === 'active' && offerTier && offerTier.active === true)) || (selectedTab === 'archived' && (offer.status === 'archived' || (offerTier && offerTier.active === false))); }).map((offer) => { @@ -179,7 +167,7 @@ export const OffersIndexModal = () => { const {discountOffer, originalPriceWithCurrency, updatedPriceWithCurrency} = getOfferDiscount(offer.type, offer.amount, offer.cadence, offer.currency || 'USD', offerTier); return ( - + handleOfferEdit(offer?.id ? offer.id : '') : () => {}}>{offer?.name}
{offerTier.name} {getOfferCadence(offer.cadence)}
handleOfferEdit(offer?.id ? offer.id : '') : () => {}}>{discountOffer}
{offer.type !== 'trial' ? getOfferDuration(offer.duration) : 'Trial period'}
handleOfferEdit(offer?.id ? offer.id : '') : () => {}}>{updatedPriceWithCurrency} {offer.type !== 'trial' ? {originalPriceWithCurrency} : null} diff --git a/apps/admin-x-settings/test/acceptance/membership/offers.test.ts b/apps/admin-x-settings/test/acceptance/membership/offers.test.ts index 4748494c25..b46007cb20 100644 --- a/apps/admin-x-settings/test/acceptance/membership/offers.test.ts +++ b/apps/admin-x-settings/test/acceptance/membership/offers.test.ts @@ -1,11 +1,11 @@ import {expect, test} from '@playwright/test'; import {globalDataRequests} from '../../utils/acceptance'; -import {mockApi, responseFixtures, settingsWithStripe, toggleLabsFlag} from '@tryghost/admin-x-framework/test/acceptance'; +import {mockApi, responseFixtures, settingsWithStripe} from '@tryghost/admin-x-framework/test/acceptance'; test.describe('Offers Modal', () => { - test.beforeEach(async () => { - toggleLabsFlag('adminXOffers', true); - }); + // test.beforeEach(async () => { + // toggleLabsFlag('adminXOffers', true); + // }); test('Offers Modal is available', async ({page}) => { await mockApi({page, requests: { diff --git a/apps/admin-x-settings/test/acceptance/membership/tiers.test.ts b/apps/admin-x-settings/test/acceptance/membership/tiers.test.ts index 73d73cfd03..51207bf8a0 100644 --- a/apps/admin-x-settings/test/acceptance/membership/tiers.test.ts +++ b/apps/admin-x-settings/test/acceptance/membership/tiers.test.ts @@ -54,8 +54,6 @@ test.describe('Tier settings', async () => { await modal.getByRole('button', {name: 'Save & close'}).click(); - await page.pause(); - // await expect(section.getByTestId('tier-card').filter({hasText: /Plus/})).toHaveText(/Plus tier/); // await expect(section.getByTestId('tier-card').filter({hasText: /Plus/})).toHaveText(/\$8\/month/); diff --git a/apps/comments-ui/test/e2e/actions.test.ts b/apps/comments-ui/test/e2e/actions.test.ts index 15d02de20a..2f2bd79e29 100644 --- a/apps/comments-ui/test/e2e/actions.test.ts +++ b/apps/comments-ui/test/e2e/actions.test.ts @@ -90,9 +90,6 @@ test.describe('Actions', async () => { await replyButton.click(); const editor = frame.getByTestId('form-editor'); await expect(editor).toBeVisible(); - - await page.pause(); - // Wait for focused await waitEditorFocused(editor); diff --git a/ghost/admin/.lint-todo b/ghost/admin/.lint-todo index 579705fe2d..976f26755e 100644 --- a/ghost/admin/.lint-todo +++ b/ghost/admin/.lint-todo @@ -351,3 +351,4 @@ add|ember-template-lint|no-passed-in-event-handlers|50|48|50|48|138f75fcc7408e61 add|ember-template-lint|no-passed-in-event-handlers|76|48|76|48|8fb95409c6333d15490f0777239f6e04641215e6|1697068800000|1707440400000|1712620800000|app/templates/settings/integrations/slack.hbs add|ember-template-lint|require-valid-alt-text|24|20|24|20|a500be6114ce681e610b6fec7d2a6cea2f4555f7|1697068800000|1707440400000|1712620800000|app/templates/settings/integrations/slack.hbs add|ember-template-lint|no-action|51|56|51|56|2b83cff3852a6f3cb1f2d2613e1bb611e1af4996|1697068800000|1707440400000|1712620800000|app/templates/settings/integrations/unsplash.hbs +remove|ember-template-lint|no-unknown-arguments-for-builtin-components|118|95|118|95|b8aae2daed1c14cf280800b3d282d11fb14851a4|1697068800000|1707440400000|1712620800000|app/components/gh-nav-menu/main.hbs diff --git a/ghost/admin/app/components/gh-nav-menu/main.hbs b/ghost/admin/app/components/gh-nav-menu/main.hbs index 036d2ce411..0e1e22c0bc 100644 --- a/ghost/admin/app/components/gh-nav-menu/main.hbs +++ b/ghost/admin/app/components/gh-nav-menu/main.hbs @@ -117,11 +117,6 @@ {{/if}} - {{#if this.settings.paidMembersEnabled}} -
  • - {{svg-jar "percentage"}}Offers -
  • - {{/if}} {{/if}} {{#if (feature "adminXDemo")}}
  • diff --git a/ghost/admin/app/router.js b/ghost/admin/app/router.js index e63f0eb076..e0f5c4e960 100644 --- a/ghost/admin/app/router.js +++ b/ghost/admin/app/router.js @@ -82,10 +82,10 @@ Router.map(function () { this.route('member', {path: '/members/:member_id'}); this.route('members-activity'); - this.route('offers'); + // this.route('offers'); - this.route('offer.new', {path: '/offers/new'}); - this.route('offer', {path: '/offers/:offer_id'}); + // this.route('offer.new', {path: '/offers/new'}); + // this.route('offer', {path: '/offers/:offer_id'}); this.route('error404', {path: '/*path'}); diff --git a/ghost/admin/app/routes/offer.js b/ghost/admin/app/routes/offer.js index d90111cffa..29516d7299 100644 --- a/ghost/admin/app/routes/offer.js +++ b/ghost/admin/app/routes/offer.js @@ -1,80 +1,80 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import ConfirmUnsavedChangesModal from '../components/modals/confirm-unsaved-changes'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; +// import AdminRoute from 'ghost-admin/routes/admin'; +// import ConfirmUnsavedChangesModal from '../components/modals/confirm-unsaved-changes'; +// import {action} from '@ember/object'; +// import {inject as service} from '@ember/service'; -export default class OffersRoute extends AdminRoute { - @service modals; - @service router; +// export default class OffersRoute extends AdminRoute { +// @service modals; +// @service router; - _requiresBackgroundRefresh = true; +// _requiresBackgroundRefresh = true; - model(params) { - this._requiresBackgroundRefresh = false; +// model(params) { +// this._requiresBackgroundRefresh = false; - if (params.offer_id) { - return this.store.queryRecord('offer', {id: params.offer_id}); - } else { - return this.store.createRecord('offer'); - } - } +// if (params.offer_id) { +// return this.store.queryRecord('offer', {id: params.offer_id}); +// } else { +// return this.store.createRecord('offer'); +// } +// } - setupController(controller, offer) { - super.setupController(...arguments); +// setupController(controller, offer) { +// super.setupController(...arguments); - if (this._requiresBackgroundRefresh) { - controller.fetchOfferTask.perform(offer.id); - } - } +// if (this._requiresBackgroundRefresh) { +// controller.fetchOfferTask.perform(offer.id); +// } +// } - deactivate() { - // clean up newly created records and revert unsaved changes to existing - this.controller.offer.rollbackAttributes(); - this._requiresBackgroundRefresh = true; - } +// deactivate() { +// // clean up newly created records and revert unsaved changes to existing +// this.controller.offer.rollbackAttributes(); +// this._requiresBackgroundRefresh = true; +// } - @action - async willTransition(transition) { - if (this.hasConfirmed) { - return true; - } +// @action +// async willTransition(transition) { +// if (this.hasConfirmed) { +// return true; +// } - transition.abort(); +// transition.abort(); - // wait for any existing confirm modal to be closed before allowing transition - if (this.confirmModal) { - return; - } +// // wait for any existing confirm modal to be closed before allowing transition +// if (this.confirmModal) { +// return; +// } - const shouldLeave = await this.confirmUnsavedChanges(); +// const shouldLeave = await this.confirmUnsavedChanges(); - if (shouldLeave) { - this.controller.model.rollbackAttributes(); - this.hasConfirmed = true; - return transition.retry(); - } - } +// if (shouldLeave) { +// this.controller.model.rollbackAttributes(); +// this.hasConfirmed = true; +// return transition.retry(); +// } +// } - async confirmUnsavedChanges() { - if (this.controller.model?.hasDirtyAttributes) { - this.confirmModal = this.modals - .open(ConfirmUnsavedChangesModal) - .finally(() => { - this.confirmModal = null; - }); +// async confirmUnsavedChanges() { +// if (this.controller.model?.hasDirtyAttributes) { +// this.confirmModal = this.modals +// .open(ConfirmUnsavedChangesModal) +// .finally(() => { +// this.confirmModal = null; +// }); - return this.confirmModal; - } +// return this.confirmModal; +// } - return true; - } +// return true; +// } - @action - save() { - this.controller.save(); - } +// @action +// save() { +// this.controller.save(); +// } - titleToken() { - return this.controller.offer.name; - } -} +// titleToken() { +// return this.controller.offer.name; +// } +// } diff --git a/ghost/admin/app/routes/offer/new.js b/ghost/admin/app/routes/offer/new.js index 50a7bb739a..43bf12f103 100644 --- a/ghost/admin/app/routes/offer/new.js +++ b/ghost/admin/app/routes/offer/new.js @@ -1,18 +1,18 @@ -import OfferRoute from '../offer'; -import {inject as service} from '@ember/service'; +// import OfferRoute from '../offer'; +// import {inject as service} from '@ember/service'; -export default class NewOfferRoute extends OfferRoute { - @service membersUtils; +// export default class NewOfferRoute extends OfferRoute { +// @service membersUtils; - controllerName = 'offer'; - templateName = 'offer'; +// controllerName = 'offer'; +// templateName = 'offer'; - /** - * First check if we have active tiers - */ - beforeModel() { - if (!this.membersUtils.hasActiveTiers) { - return this.replaceWith('offers'); - } - } -} +// /** +// * First check if we have active tiers +// */ +// beforeModel() { +// if (!this.membersUtils.hasActiveTiers) { +// return this.replaceWith('offers'); +// } +// } +// } diff --git a/ghost/admin/app/routes/offers.js b/ghost/admin/app/routes/offers.js index 152ba3386e..bbb67cbbcb 100644 --- a/ghost/admin/app/routes/offers.js +++ b/ghost/admin/app/routes/offers.js @@ -1,31 +1,31 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import {inject as service} from '@ember/service'; +// import AdminRoute from 'ghost-admin/routes/admin'; +// import {inject as service} from '@ember/service'; -export default class offersRoute extends AdminRoute { - @service store; - @service feature; +// export default class offersRoute extends AdminRoute { +// @service store; +// @service feature; - queryParams = { - type: {refreshModel: true} - }; +// queryParams = { +// type: {refreshModel: true} +// }; - beforeModel() { - super.beforeModel(...arguments); - // TODO: redirect if members is disabled? - } +// beforeModel() { +// super.beforeModel(...arguments); +// // TODO: redirect if members is disabled? +// } - model(params) { - return this.controllerFor('offers').fetchOffersTask.perform(params); - } +// model(params) { +// return this.controllerFor('offers').fetchOffersTask.perform(params); +// } - // trigger a background load of members plus labels for filter dropdown - setupController() { - super.setupController(...arguments); - } +// // trigger a background load of members plus labels for filter dropdown +// setupController() { +// super.setupController(...arguments); +// } - buildRouteInfoMetadata() { - return { - titleToken: 'Offers' - }; - } -} +// buildRouteInfoMetadata() { +// return { +// titleToken: 'Offers' +// }; +// } +// } diff --git a/ghost/admin/tests/acceptance/offers-test.js b/ghost/admin/tests/acceptance/offers-test.js index 54a45769a5..f3e7058b49 100644 --- a/ghost/admin/tests/acceptance/offers-test.js +++ b/ghost/admin/tests/acceptance/offers-test.js @@ -1,111 +1,111 @@ -import moment from 'moment-timezone'; -import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support'; -import {beforeEach, describe, it} from 'mocha'; -import {blur, click, currentURL, fillIn, find, findAll, settled} from '@ember/test-helpers'; -import {enablePaidMembers} from '../helpers/members'; -import {expect} from 'chai'; -import {setupApplicationTest} from 'ember-mocha'; -import {setupMirage} from 'ember-cli-mirage/test-support'; -import {timeout} from 'ember-concurrency'; -import {visit} from '../helpers/visit'; +// import moment from 'moment-timezone'; +// import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support'; +// import {beforeEach, describe, it} from 'mocha'; +// import {blur, click, currentURL, fillIn, find, findAll, settled} from '@ember/test-helpers'; +// import {enablePaidMembers} from '../helpers/members'; +// import {expect} from 'chai'; +// import {setupApplicationTest} from 'ember-mocha'; +// import {setupMirage} from 'ember-cli-mirage/test-support'; +// import {timeout} from 'ember-concurrency'; +// import {visit} from '../helpers/visit'; -describe('Acceptance: Offers', function () { - let hooks = setupApplicationTest(); - setupMirage(hooks); +// describe('Acceptance: Offers', function () { +// let hooks = setupApplicationTest(); +// setupMirage(hooks); - it('redirects to signin when not authenticated', async function () { - await invalidateSession(); - await visit('/offers'); +// it('redirects to signin when not authenticated', async function () { +// await invalidateSession(); +// await visit('/offers'); - expect(currentURL()).to.equal('/signin'); - }); +// expect(currentURL()).to.equal('/signin'); +// }); - it('redirects non-admins to site', async function () { - let role = this.server.create('role', {name: 'Editor'}); - this.server.create('user', {roles: [role]}); +// it('redirects non-admins to site', async function () { +// let role = this.server.create('role', {name: 'Editor'}); +// this.server.create('user', {roles: [role]}); - await authenticateSession(); - await visit('/offers'); +// await authenticateSession(); +// await visit('/offers'); - expect(currentURL()).to.equal('/site'); - expect(find('[data-test-nav="offers"]'), 'sidebar link') - .to.not.exist; - }); +// expect(currentURL()).to.equal('/site'); +// expect(find('[data-test-nav="offers"]'), 'sidebar link') +// .to.not.exist; +// }); - describe('as owner', function () { - beforeEach(async function () { - this.server.loadFixtures('tiers'); +// describe('as owner', function () { +// beforeEach(async function () { +// this.server.loadFixtures('tiers'); - let role = this.server.create('role', {name: 'Owner'}); - this.server.create('user', {roles: [role]}); +// let role = this.server.create('role', {name: 'Owner'}); +// this.server.create('user', {roles: [role]}); - enablePaidMembers(this.server); +// enablePaidMembers(this.server); - return await authenticateSession(); - }); +// return await authenticateSession(); +// }); - it('it renders, can be navigated, can edit offer', async function () { - const tier = this.server.create('tier'); - let offer1 = this.server.create('offer', {tier: {id: tier.id}, createdAt: moment.utc().subtract(1, 'day').valueOf()}); - this.server.create('offer', {tier: {id: tier.id}, createdAt: moment.utc().subtract(2, 'day').valueOf()}); +// it('it renders, can be navigated, can edit offer', async function () { +// const tier = this.server.create('tier'); +// let offer1 = this.server.create('offer', {tier: {id: tier.id}, createdAt: moment.utc().subtract(1, 'day').valueOf()}); +// this.server.create('offer', {tier: {id: tier.id}, createdAt: moment.utc().subtract(2, 'day').valueOf()}); - await visit('/offers'); +// await visit('/offers'); - await settled(); +// await settled(); - // lands on correct page - expect(currentURL(), 'currentURL').to.equal('/offers'); +// // lands on correct page +// expect(currentURL(), 'currentURL').to.equal('/offers'); - // it highlights active state in nav menu - expect( - find('[data-test-nav="offers"]'), - 'highlights nav menu item' - ).to.have.class('active'); +// // it highlights active state in nav menu +// expect( +// find('[data-test-nav="offers"]'), +// 'highlights nav menu item' +// ).to.have.class('active'); - // it lists all offers - expect(findAll('[data-test-list="offers-list-item"]').length, 'offers list count') - .to.equal(2); +// // it lists all offers +// expect(findAll('[data-test-list="offers-list-item"]').length, 'offers list count') +// .to.equal(2); - let offer = find('[data-test-list="offers-list-item"]'); - expect(offer.querySelector('[data-test-list="offer-name"] h3').textContent, 'offer list item name') - .to.equal(offer1.name); +// let offer = find('[data-test-list="offers-list-item"]'); +// expect(offer.querySelector('[data-test-list="offer-name"] h3').textContent, 'offer list item name') +// .to.equal(offer1.name); - await visit(`/offers/${offer1.id}`); +// await visit(`/offers/${offer1.id}`); - // second wait is needed for the offer details to settle - await settled(); +// // second wait is needed for the offer details to settle +// await settled(); - // it shows selected offer form - expect(find('[data-test-input="offer-name"]').value, 'loads correct offer into form') - .to.equal(offer1.name); +// // it shows selected offer form +// expect(find('[data-test-input="offer-name"]').value, 'loads correct offer into form') +// .to.equal(offer1.name); - // it maintains active state in nav menu - expect( - find('[data-test-nav="offers"]'), - 'highlights nav menu item' - ).to.have.class('active'); +// // it maintains active state in nav menu +// expect( +// find('[data-test-nav="offers"]'), +// 'highlights nav menu item' +// ).to.have.class('active'); - // trigger save - await fillIn('[data-test-input="offer-name"]', 'New Name'); - await blur('[data-test-input="offer-name"]'); +// // trigger save +// await fillIn('[data-test-input="offer-name"]', 'New Name'); +// await blur('[data-test-input="offer-name"]'); - await click('[data-test-button="save"]'); +// await click('[data-test-button="save"]'); - // extra timeout needed for Travis - sometimes it doesn't update - // quick enough and an extra wait() call doesn't help - await timeout(100); +// // extra timeout needed for Travis - sometimes it doesn't update +// // quick enough and an extra wait() call doesn't help +// await timeout(100); - await click('[data-test-link="offers-back"]'); +// await click('[data-test-link="offers-back"]'); - // lands on correct page - expect(currentURL(), 'currentURL').to.equal('/offers'); - }); +// // lands on correct page +// expect(currentURL(), 'currentURL').to.equal('/offers'); +// }); - it('maintains active state in nav menu when creating a new tag', async function () { - await visit('offers/new'); - expect(currentURL()).to.equal('offers/new'); - expect(find('[data-test-nav="offers"]'), 'highlights nav menu item') - .to.have.class('active'); - }); - }); -}); +// it('maintains active state in nav menu when creating a new tag', async function () { +// await visit('offers/new'); +// expect(currentURL()).to.equal('offers/new'); +// expect(find('[data-test-nav="offers"]'), 'highlights nav menu item') +// .to.have.class('active'); +// }); +// }); +// }); diff --git a/ghost/core/core/shared/labs.js b/ghost/core/core/shared/labs.js index 745bf50dd8..4e3c4e601c 100644 --- a/ghost/core/core/shared/labs.js +++ b/ghost/core/core/shared/labs.js @@ -44,7 +44,7 @@ const ALPHA_FEATURES = [ 'tipsAndDonations', 'importMemberTier', 'lexicalIndicators', - 'adminXOffers', + // 'adminXOffers', 'filterEmailDisabled', 'adminXDemo', 'newEmailAddresses', diff --git a/ghost/core/test/e2e-browser/admin/tiers.spec.js b/ghost/core/test/e2e-browser/admin/tiers.spec.js index 7bce02a0d6..28af174f5c 100644 --- a/ghost/core/test/e2e-browser/admin/tiers.spec.js +++ b/ghost/core/test/e2e-browser/admin/tiers.spec.js @@ -23,19 +23,16 @@ test.describe('Admin', () => { monthlyPrice: 5, yearlyPrice: 50 }); - const offerName = await createOffer(sharedPage, { + const {offerName} = await createOffer(sharedPage, { name: 'Get 5% Off!', tierName, - offerType: 'discount', - amount: 5 + offerType: 'freeTrial', + amount: 14 }); - await test.step('Check that offers and tiers are available on Offers page', async () => { - await sharedPage.locator('[data-test-nav="offers"]').click(); - await sharedPage.waitForSelector('[data-test-offers-list]'); - await expect(sharedPage.locator('[data-test-offers-list]')).toContainText(tierName); - await expect(sharedPage.locator('[data-test-offers-list]')).toContainText(offerName); - }); + await sharedPage.goto('/ghost/'); + await sharedPage.goto('/ghost/#/settings/offers'); + await expect(sharedPage.getByTestId('offers')).toContainText(offerName); }); test('Can create additional Tier', async ({sharedPage}) => { diff --git a/ghost/core/test/e2e-browser/portal/offers.spec.js b/ghost/core/test/e2e-browser/portal/offers.spec.js index e63569ed73..06a9a9ef11 100644 --- a/ghost/core/test/e2e-browser/portal/offers.spec.js +++ b/ghost/core/test/e2e-browser/portal/offers.spec.js @@ -18,7 +18,7 @@ test.describe('Portal', () => { }); // add a new offer with free trial - const offerName = await createOffer(sharedPage, { + const {offerName, offerLink} = await createOffer(sharedPage, { name: 'Black Friday Special', tierName, offerType: 'freeTrial', @@ -26,14 +26,10 @@ test.describe('Portal', () => { }); // check that offer was added in the offer list screen - await expect(sharedPage.locator(`[data-test-offer="${offerName}"]`), 'Should have free-trial offer').toBeVisible(); + await sharedPage.goto('/ghost/#/settings/offers'); + await expect(sharedPage.getByTestId('offers')).toContainText(offerName); - // open offer details page - await sharedPage.locator(`[data-test-offer="${offerName}"] a`).first().click(); - - // fetch offer url from portal settings and open it - const portalUrl = await sharedPage.locator('[data-test-input="offer-portal-url"]').inputValue(); - await sharedPage.goto(portalUrl); + await sharedPage.goto(offerLink); const portalTriggerButton = sharedPage.frameLocator('[data-testid="portal-trigger-frame"]').locator('[data-testid="portal-trigger-button"]'); const portalFrame = sharedPage.frameLocator('[data-testid="portal-popup-frame"]'); @@ -66,14 +62,15 @@ test.describe('Portal', () => { await sharedPage.goto('/ghost'); await sharedPage.locator('.gh-nav a[href="#/members/"]').click(); - // 1 member, should be Testy, on Portal Tier + // // 1 member, should be Testy, on Portal Tier await expect(sharedPage.getByRole('link', {name: 'Testy McTesterson testy+trial@example.com'}), 'Should have 1 paid member').toBeVisible(); await expect(sharedPage.getByRole('link', {name: tierName}), `Paid member should be on ${tierName}`).toBeVisible(); - // Ensure the offer redemption count was bumped - await sharedPage.locator('.gh-nav a[href="#/offers/"]').click(); - const locator = sharedPage.locator(`[data-test-offer="${offerName}"]`).locator('[data-test-list="redemption-count"]').locator('span'); - await expect(locator).toContainText('1'); + // // Ensure the offer redemption count was bumped + await sharedPage.goto('/ghost/#/settings/offers'); + // await sharedPage.locator('.gh-nav a[href="#/offers/"]').click(); + const locator = sharedPage.locator(`[data-test-offer="${offerName}"]`); + await expect(locator).toContainText('1 redemption'); }); test('Creates and uses a one-time discount Offer', async ({sharedPage}) => { @@ -90,7 +87,7 @@ test.describe('Portal', () => { }); // Creates a one-time discount offer for 10% off - const offerName = await createOffer(sharedPage, { + const {offerName, offerLink} = await createOffer(sharedPage, { name: 'Black Friday Special', tierName: tierName, offerType: 'discount', @@ -98,14 +95,13 @@ test.describe('Portal', () => { }); // check that offer was added in the offer list screen - await expect(sharedPage.locator(`[data-test-offer="${offerName}"]`), 'Should have free-trial offer').toBeVisible(); - + await sharedPage.goto('/ghost/#/settings/offers'); + await expect(sharedPage.getByTestId('offers')).toContainText(offerName); // open offer details page - await sharedPage.locator(`[data-test-offer="${offerName}"] a`).first().click(); + // await sharedPage.locator(`[data-test-offer="${offerName}"] a`).first().click(); // fetch offer url from portal settings and open it - const portalUrl = await sharedPage.locator('[data-test-input="offer-portal-url"]').inputValue(); - await sharedPage.goto(portalUrl); + await sharedPage.goto(offerLink); const portalTriggerButton = sharedPage.frameLocator('[data-testid="portal-trigger-frame"]').locator('[data-testid="portal-trigger-button"]'); const portalFrame = sharedPage.frameLocator('[data-testid="portal-popup-frame"]'); @@ -155,7 +151,7 @@ test.describe('Portal', () => { }); // Creates a one-time discount offer for 10% off - const offerName = await createOffer(sharedPage, { + const {offerName, offerLink} = await createOffer(sharedPage, { name: 'Black Friday Special', tierName: tierName, offerType: 'discount', @@ -165,14 +161,10 @@ test.describe('Portal', () => { }); // check that offer was added in the offer list screen - await expect(sharedPage.locator(`[data-test-offer="${offerName}"]`), 'Should have free-trial offer').toBeVisible(); + await sharedPage.goto('/ghost/#/settings/offers'); + await expect(sharedPage.getByTestId('offers')).toContainText(offerName); - // open offer details page - await sharedPage.locator(`[data-test-offer="${offerName}"] a`).first().click(); - - // fetch offer url from portal settings and open it - const portalUrl = await sharedPage.locator('[data-test-input="offer-portal-url"]').inputValue(); - await sharedPage.goto(portalUrl); + await sharedPage.goto(offerLink); const portalTriggerButton = sharedPage.frameLocator('[data-testid="portal-trigger-frame"]').locator('[data-testid="portal-trigger-button"]'); const portalFrame = sharedPage.frameLocator('[data-testid="portal-popup-frame"]'); @@ -223,7 +215,7 @@ test.describe('Portal', () => { }); // Creates a one-time discount offer for 10% off - const offerName = await createOffer(sharedPage, { + const {offerName, offerLink} = await createOffer(sharedPage, { name: 'Black Friday Special', tierName: tierName, offerType: 'discount', @@ -232,14 +224,10 @@ test.describe('Portal', () => { }); // check that offer was added in the offer list screen - await expect(sharedPage.locator(`[data-test-offer="${offerName}"]`), 'Should have free-trial offer').toBeVisible(); + await sharedPage.goto('/ghost/#/settings/offers'); + await expect(sharedPage.getByTestId('offers')).toContainText(offerName); - // open offer details page - await sharedPage.locator(`[data-test-offer="${offerName}"] a`).first().click(); - - // fetch offer url from portal settings and open it - const portalUrl = await sharedPage.locator('[data-test-input="offer-portal-url"]').inputValue(); - await sharedPage.goto(portalUrl); + await sharedPage.goto(offerLink); const portalTriggerButton = sharedPage.frameLocator('[data-testid="portal-trigger-frame"]').locator('[data-testid="portal-trigger-button"]'); const portalFrame = sharedPage.frameLocator('[data-testid="portal-popup-frame"]'); @@ -286,7 +274,7 @@ test.describe('Portal', () => { }); // Create an offer. This will be archived - const offerName = await createOffer(sharedPage, { + const {offerLink} = await createOffer(sharedPage, { name: 'To be archived', tierName: tierName, offerType: 'discount', @@ -300,18 +288,8 @@ test.describe('Portal', () => { offerType: 'discount', amount: 10 }); - - // Check if the offer appears in the archive list - await sharedPage.locator('.gh-contentfilter-menu-trigger').click(); - await sharedPage.getByRole('option', {name: 'Archived offers'}).click(); - await expect(sharedPage.getByRole('link', {name: offerName}), 'Should have an archived offer').toBeVisible(); - - // Go to the offer and grab the offer URL - await sharedPage.locator('.gh-offers-list .gh-list-row').filter({hasText: offerName}).click(); - const portalUrl = await sharedPage.locator('input#url').inputValue(); - // Open the offer URL and make sure portal popup doesn't load - await sharedPage.goto(portalUrl); + await sharedPage.goto(offerLink); const portalPopup = await sharedPage.locator('[data-testid="portal-popup-frame"]').isVisible(); await expect(portalPopup).toBeFalsy(); }); diff --git a/ghost/core/test/e2e-browser/utils/e2e-browser-utils.js b/ghost/core/test/e2e-browser/utils/e2e-browser-utils.js index 4247090356..67e69423c7 100644 --- a/ghost/core/test/e2e-browser/utils/e2e-browser-utils.js +++ b/ghost/core/test/e2e-browser/utils/e2e-browser-utils.js @@ -247,72 +247,71 @@ const createTier = async (page, {name, monthlyPrice, yearlyPrice, trialDays}, en * @param {string} [options.discountType] * @param {number} [options.discountDuration] * @param {number} options.amount - * @returns {Promise} Unique offer name + * @returns {Promise} Unique offer name */ + const createOffer = async (page, {name, tierName, offerType, amount, discountType = null, discountDuration = 3}) => { await page.goto('/ghost'); - await page.locator('.gh-nav a[href="#/offers/"]').click(); + await page.locator('[data-test-nav="settings"]').click(); // Keep offer names unique & <= 40 characters let offerName = `${name} (${new ObjectID().toHexString().slice(0, 40 - name.length - 3)})`; + // Tiers request can take time, so waiting until there is no connections before interacting with them + await page.waitForLoadState('networkidle'); + const hasExistingOffers = await page.getByTestId('offers').getByRole('button', {name: 'Manage offers'}).isVisible(); + const isCTA = await page.getByTestId('offers').getByRole('button', {name: 'Add offer'}).isVisible(); // Archive other offers to keep the list tidy // We only need 1 offer to be active at a time // Either the list of active offers loads, or the CTA when no offers exist - while (await Promise.race([ - page.locator('.gh-offers-list .gh-list-header').filter({hasText: 'active'}).waitFor({state: 'visible', timeout: 1000}).then(() => true), - page.locator('.gh-offers-list-cta').waitFor({state: 'visible', timeout: 1000}).then(() => false) - ]).catch(() => false)) { - const listItem = page.locator('.gh-offers-list .gh-list-row:not(.header)').first(); - await listItem.locator('a[href^="#/offers/"]').last().click(); - await page.getByRole('button', {name: 'Archive offer'}).click(); - await page - .locator('.modal-content') - .filter({hasText: 'Archive offer'}) - .first() - .getByRole('button', {name: 'Archive'}) - .click(); + if (hasExistingOffers && !isCTA) { + await page.getByTestId('offers').getByRole('button', {name: 'Manage offers'}).click(); + await page.waitForLoadState('networkidle'); - // TODO: Use a more resilient selector - const statusDropdown = await page.getByRole('button', {name: 'Archived offers'}); - await statusDropdown.waitFor({ - state: 'visible', - timeout: 1000 - }); - await statusDropdown.click(); - await page.getByRole('option', {name: 'Active offers'}).click(); + // Selector for the elements with data-testid 'offer-item' + // const offerItemsSelector = '[data-testid="offer-item"]'; + await page.getByTestId('offer-item').nth(0).click(); + await page.getByRole('button', {name: 'Archive offer'}).click(); + + const confirmModal = page.getByTestId('confirmation-modal'); + await confirmModal.getByRole('button', {name: 'Archive'}).click(); } - await page.getByRole('link', {name: 'New offer'}).click(); - await page.locator('input#name').fill(offerName); + if (isCTA) { + await page.getByTestId('offers').getByRole('button', {name: 'Add offer'}).click(); + } else { + await page.getByText('New offer').click(); + } + + // const newOfferButton = await page.getByTestId('offers').getByRole('button', {name: 'Add offer'}) || await page.getByTestId('offers').getByRole('button', {name: 'New offer'}); + // await page.getByTestId('offers').getByRole('button', {name: 'Add offer'}).click(); + // await newOfferButton.click(); + await page.getByLabel('Offer name').fill(offerName); if (offerType === 'freeTrial') { - await page.getByRole('button', {name: 'Free trial Give free access for a limited time.'}).click(); - await page.locator('input#trial-duration').fill(`${amount}`); + // await page.getByRole('button', {name: 'Free trial Give free access for a limited time.'}).click(); + await page.getByText('Give free access for a limited time').click(); + await page.getByLabel('Trial duration').fill(`${amount}`); } else if (offerType === 'discount') { - await page.locator('input#amount').fill(`${amount}`); + await page.getByLabel('Amount off').fill(`${amount}`); if (discountType === 'multiple-months') { - await page.locator('[data-test-select="offer-duration"]').selectOption('repeating'); - await page.locator('input#duration-months').fill(discountDuration.toString()); + await chooseOptionInSelect(page.getByTestId('duration-select-offers'), `Multiple-months`); + await page.getByLabel('Duration in months').fill(discountDuration.toString()); + // await page.locator('[data-test-select="offer-duration"]').selectOption('repeating'); + // await page.locator('input#duration-months').fill(discountDuration.toString()); } if (discountType === 'forever') { - await page.locator('[data-test-select="offer-duration"]').selectOption('forever'); + await chooseOptionInSelect(page.getByTestId('duration-select-offers'), `Forever`); } } - const priceId = await page.locator(`.gh-select-product-cadence>select>option`).getByText(`${tierName} - Monthly`).getAttribute('value'); - await page.locator('.gh-select-product-cadence>select').selectOption(priceId); + await chooseOptionInSelect(page.getByTestId('tier-cadence-select-offers'), `${tierName} - Monthly`); + await page.getByRole('button', {name: 'Publish'}).click(); + await page.waitForLoadState('networkidle'); + const offerLink = await page.locator('input[name="offer-url"]').inputValue(); - await page.getByRole('button', {name: 'Save'}).click(); - // Wait for the "Saved" button, ensures that next clicks don't trigger the unsaved work modal - await page.getByRole('button', {name: 'Saved'}).waitFor({ - state: 'visible', - timeout: 10000 - }); - await page.locator('.gh-nav a[href="#/offers/"]').click(); - - return offerName; + return {offerName, offerLink}; }; const fillInputIfExists = async (page, selector, value) => { @@ -493,6 +492,11 @@ const generateStripeIntegrationToken = async (accountId) => { })).toString('base64'); }; +const chooseOptionInSelect = async (select, optionText) => { + await select.click(); + await select.page().locator('[data-testid="select-option"]', {hasText: optionText}).click(); +}; + module.exports = { setupGhost, setupStripe, @@ -509,5 +513,6 @@ module.exports = { completeStripeSubscription, impersonateMember, goToMembershipPage, - openTierModal + openTierModal, + chooseOptionInSelect };