Skip to content

Commit

Permalink
feat(experiments): Can undo previous variant's transforms when viewin…
Browse files Browse the repository at this point in the history
…g control (#25956)

No-code experiments: Can undo previous variant's transforms when viewing control.

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
Phanatic and github-actions[bot] authored Nov 2, 2024
1 parent 89fb532 commit e501e5e
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 || {})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export function WebExperimentTransformField({
}
setExperimentFormValue('variants', experimentForm.variants)
}}
value={transform.css}
value={transform.css || ''}
/>
)}
</>
Expand Down
137 changes: 116 additions & 21 deletions frontend/src/toolbar/experiments/experimentsTabLogic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<b> Hello world! </b>',
selector: 'h1',
text: 'Hello world',
},
],
},
test2: {
transforms: [
{
html: '<b> Goodbye world! </b>',
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<typeof experimentsTabLogic.build>
let theExperimentsLogic: ReturnType<typeof experimentsLogic.build>
Expand All @@ -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/': () => ({
Expand All @@ -53,6 +87,9 @@ describe('experimentsTabLogic', () => {
})
initKeaTests()

theExperimentsLogic = experimentsLogic()
theExperimentsLogic.mount()

theExperimentsTabLogic = experimentsTabLogic()
theExperimentsTabLogic.mount()

Expand All @@ -61,9 +98,6 @@ describe('experimentsTabLogic', () => {

theToolbarConfigLogic = toolbarConfigLogic({ apiURL: 'http://localhost' })
theToolbarConfigLogic.mount()

theExperimentsLogic = experimentsLogic()
theExperimentsLogic.mount()
})

describe('core assumptions', () => {
Expand Down Expand Up @@ -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<Element>
}

return elParent
}

describe('editing experiments', () => {
it('can edit an existing experiment', async () => {
await expectLogic(theExperimentsTabLogic, () => {
Expand All @@ -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')
})
})
})
})
48 changes: 45 additions & 3 deletions frontend/src/toolbar/experiments/experimentsTabLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ export const experimentsTabLogic = kea<experimentsTabLogicType>([
removeVariant: (variant: string) => ({
variant,
}),
applyVariant: (variant: string) => ({
applyVariant: (current_variant: string, variant: string) => ({
current_variant,
variant,
}),
addNewElement: (variant: string) => ({ variant }),
Expand Down Expand Up @@ -144,6 +145,10 @@ export const experimentsTabLogic = kea<experimentsTabLogicType>([
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

Expand Down Expand Up @@ -280,11 +285,16 @@ export const experimentsTabLogic = kea<experimentsTabLogicType>([
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) => {
Expand All @@ -305,6 +315,38 @@ export const experimentsTabLogic = kea<experimentsTabLogicType>([
})
}
})

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)
}
}
})
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/toolbar/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export type ExperimentDraftType = Omit<Experiment, 'id' | 'created_at' | 'create

export interface ExperimentForm extends ExperimentDraftType {
variants?: Record<string, WebExperimentVariant>
undo_transforms?: WebExperimentTransform[]
}

export interface ActionStepForm extends ActionStepType {
Expand Down Expand Up @@ -117,5 +118,5 @@ export interface WebExperimentTransform {
text?: string
html?: string
imgUrl?: string
css?: string
css?: string | null
}

0 comments on commit e501e5e

Please sign in to comment.