diff --git a/frontend/src/toolbar/experiments/ExperimentsEditingToolbarMenu.tsx b/frontend/src/toolbar/experiments/ExperimentsEditingToolbarMenu.tsx index bdbafabc35ff9..420754e9e101c 100644 --- a/frontend/src/toolbar/experiments/ExperimentsEditingToolbarMenu.tsx +++ b/frontend/src/toolbar/experiments/ExperimentsEditingToolbarMenu.tsx @@ -84,7 +84,7 @@ export const ExperimentsEditingToolbarMenu = (): JSX.Element => { onChange={(variant) => { if (variant) { selectVariant(variant) - applyVariant(variant) + applyVariant(selectedVariant, variant) } }} panels={Object.keys(experimentForm.variants || {}) diff --git a/frontend/src/toolbar/experiments/WebExperimentTransformField.tsx b/frontend/src/toolbar/experiments/WebExperimentTransformField.tsx index 1b9da75065393..b89838fa45c32 100644 --- a/frontend/src/toolbar/experiments/WebExperimentTransformField.tsx +++ b/frontend/src/toolbar/experiments/WebExperimentTransformField.tsx @@ -155,7 +155,7 @@ export function WebExperimentTransformField({ } setExperimentFormValue('variants', experimentForm.variants) }} - value={transform.css} + value={transform.css || ''} /> )} diff --git a/frontend/src/toolbar/experiments/experimentsTabLogic.test.ts b/frontend/src/toolbar/experiments/experimentsTabLogic.test.ts index 8c1e2f337eea0..2919f28e255da 100644 --- a/frontend/src/toolbar/experiments/experimentsTabLogic.test.ts +++ b/frontend/src/toolbar/experiments/experimentsTabLogic.test.ts @@ -10,6 +10,57 @@ import { WebExperimentTransform } from '~/toolbar/types' import { experimentsLogic } from './experimentsLogic' import { experimentsTabLogic } from './experimentsTabLogic' +const web_experiments = [ + { + id: 1, + name: 'Test Experiment 1', + variants: { + control: { + transforms: [ + { + html: '', + selector: 'h1', + text: '', + }, + ], + }, + }, + }, + { + id: 2, + name: 'Test Experiment 2', + variants: { + control: { + transforms: [], + }, + test: { + transforms: [ + { + html: ' Hello world! ', + selector: 'h1', + text: 'Hello world', + }, + ], + }, + test2: { + transforms: [ + { + html: ' Goodbye world! ', + selector: 'h1', + text: 'Goodbye world', + }, + ], + }, + }, + }, +] + +global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ results: web_experiments }), + } as any as Response) +) describe('experimentsTabLogic', () => { let theExperimentsTabLogic: ReturnType let theExperimentsLogic: ReturnType @@ -19,24 +70,7 @@ describe('experimentsTabLogic', () => { beforeEach(() => { useMocks({ get: { - '/api/projects/:team/web_experiments/': () => [ - { - id: 1, - name: 'Test Experiment 1', - variants: { - control: { - transforms: [ - { - html: '', - selector: 'h1', - text: '', - }, - ], - }, - }, - }, - { id: 2, name: 'Test Experiment 2' }, - ], + '/api/projects/:team/web_experiments/': () => web_experiments, }, post: { '/api/projects/@current/web_experiments/': () => ({ @@ -53,6 +87,9 @@ describe('experimentsTabLogic', () => { }) initKeaTests() + theExperimentsLogic = experimentsLogic() + theExperimentsLogic.mount() + theExperimentsTabLogic = experimentsTabLogic() theExperimentsTabLogic.mount() @@ -61,9 +98,6 @@ describe('experimentsTabLogic', () => { theToolbarConfigLogic = toolbarConfigLogic({ apiURL: 'http://localhost' }) theToolbarConfigLogic.mount() - - theExperimentsLogic = experimentsLogic() - theExperimentsLogic.mount() }) describe('core assumptions', () => { @@ -199,6 +233,22 @@ describe('experimentsTabLogic', () => { }) }) + const createTestDocument = (): HTMLSpanElement => { + const elTarget = document.createElement('img') + elTarget.id = 'primary_button' + + const elParent = document.createElement('span') + elParent.innerText = 'original' + elParent.className = 'original' + elParent.appendChild(elTarget) + + document.querySelectorAll = function () { + return [elParent] as unknown as NodeListOf + } + + return elParent + } + describe('editing experiments', () => { it('can edit an existing experiment', async () => { await expectLogic(theExperimentsTabLogic, () => { @@ -211,5 +261,50 @@ describe('experimentsTabLogic', () => { experimentForm: { name: 'Updated Experiment 1', variants: {} }, }) }) + + it('can apply changes from a variant', async () => { + await expectLogic(theExperimentsLogic, () => { + theExperimentsLogic.actions.getExperiments() + }) + .delay(0) + .then(() => { + theExperimentsTabLogic.actions.selectExperiment(2) + const element = createTestDocument() + theExperimentsTabLogic.actions.applyVariant('', 'test') + expect(element.innerText).toEqual('Hello world') + }) + }) + + it('can switch between variants', async () => { + await expectLogic(theExperimentsLogic, () => { + theExperimentsLogic.actions.getExperiments() + }) + .delay(0) + .then(() => { + theExperimentsTabLogic.actions.selectExperiment(2) + const element = createTestDocument() + theExperimentsTabLogic.actions.applyVariant('', 'test') + expect(element.innerText).toEqual('Hello world') + theExperimentsTabLogic.actions.applyVariant('test', 'test2') + expect(element.innerText).toEqual('Goodbye world') + }) + }) + + it('can reset to control', async () => { + await expectLogic(theExperimentsLogic, () => { + theExperimentsLogic.actions.getExperiments() + }) + .delay(0) + .then(() => { + theExperimentsTabLogic.actions.selectExperiment(2) + const element = createTestDocument() + theExperimentsTabLogic.actions.applyVariant('', 'test') + expect(element.innerText).toEqual('Hello world') + theExperimentsTabLogic.actions.applyVariant('test', 'test2') + expect(element.innerText).toEqual('Goodbye world') + theExperimentsTabLogic.actions.applyVariant('test2', 'control') + expect(element.innerText).toEqual('original') + }) + }) }) }) diff --git a/frontend/src/toolbar/experiments/experimentsTabLogic.tsx b/frontend/src/toolbar/experiments/experimentsTabLogic.tsx index d5c9dc856eaa3..3b11295a6acb9 100644 --- a/frontend/src/toolbar/experiments/experimentsTabLogic.tsx +++ b/frontend/src/toolbar/experiments/experimentsTabLogic.tsx @@ -61,7 +61,8 @@ export const experimentsTabLogic = kea([ removeVariant: (variant: string) => ({ variant, }), - applyVariant: (variant: string) => ({ + applyVariant: (current_variant: string, variant: string) => ({ + current_variant, variant, }), addNewElement: (variant: string) => ({ variant }), @@ -144,6 +145,10 @@ export const experimentsTabLogic = kea([ const experimentToSave = { ...formValues, } + + // this property is used in the editor to undo transforms + // don't need to roundtrip this to the server. + delete experimentToSave.undo_transforms const { apiURL, temporaryToken } = values const { selectedExperimentId } = values @@ -280,11 +285,16 @@ export const experimentsTabLogic = kea([ actions.rebalanceRolloutPercentage() } }, - applyVariant: ({ variant }) => { + applyVariant: ({ current_variant, variant }) => { if (values.experimentForm && values.experimentForm.variants) { const selectedVariant = values.experimentForm.variants[variant] if (selectedVariant) { - selectedVariant.transforms.forEach((transform) => { + if (values.experimentForm.undo_transforms === undefined) { + values.experimentForm.undo_transforms = [] + } + + // run the undo transforms first. + values.experimentForm.undo_transforms?.forEach((transform) => { if (transform.selector) { const elements = document.querySelectorAll(transform.selector) elements.forEach((elements) => { @@ -305,6 +315,38 @@ export const experimentsTabLogic = kea([ }) } }) + + selectedVariant.transforms?.forEach((transform) => { + if (transform.selector) { + const undoTransform: WebExperimentTransform = { + selector: transform.selector, + } + const elements = document.querySelectorAll(transform.selector) + elements.forEach((elements) => { + const htmlElement = elements as HTMLElement + if (htmlElement) { + if (transform.text) { + undoTransform.text = htmlElement.innerText + htmlElement.innerText = transform.text + } + + if (transform.html) { + undoTransform.html = htmlElement.innerHTML + htmlElement.innerHTML = transform.html + } + + if (transform.css) { + undoTransform.css = htmlElement.getAttribute('style') || ' ' + htmlElement.setAttribute('style', transform.css) + } + } + }) + + if ((current_variant === 'control' || current_variant === '') && variant !== 'control') { + values.experimentForm.undo_transforms?.push(undoTransform) + } + } + }) } } }, diff --git a/frontend/src/toolbar/types.ts b/frontend/src/toolbar/types.ts index 773bd9d0a2ce2..f3c1d116b3e54 100644 --- a/frontend/src/toolbar/types.ts +++ b/frontend/src/toolbar/types.ts @@ -73,6 +73,7 @@ export type ExperimentDraftType = Omit + undo_transforms?: WebExperimentTransform[] } export interface ActionStepForm extends ActionStepType { @@ -117,5 +118,5 @@ export interface WebExperimentTransform { text?: string html?: string imgUrl?: string - css?: string + css?: string | null }