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
}