Skip to content

Commit 95033aa

Browse files
committed
Duplicate experiment action
1 parent 680d5d5 commit 95033aa

5 files changed

Lines changed: 182 additions & 8 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace Thoughtco\StatamicABTester\Actions;
4+
5+
use Statamic\Actions\Action;
6+
use Statamic\Support\Arr;
7+
use Thoughtco\StatamicABTester\Contracts\Experiment;
8+
use Thoughtco\StatamicABTester\Facades\Experiment as ExperimentFacade;
9+
10+
class DuplicateExperiment extends Action
11+
{
12+
protected $lastCreated;
13+
14+
public function run($items, $values)
15+
{
16+
$items->each(function ($experiment) {
17+
$copy = ExperimentFacade::make()
18+
->title(__('Copy of :title', ['title' => $experiment->title()]))
19+
->type($experiment->type())
20+
->goals($experiment->goals())
21+
->data(Arr::removeNullValues($experiment->data()->all()))
22+
->published(false);
23+
24+
$copy->save();
25+
26+
$this->lastCreated = $copy;
27+
});
28+
29+
return trans_choice('Experiment duplicated|Experiments duplicated', $items->count());
30+
}
31+
32+
public function redirect($items, $values)
33+
{
34+
if ($items->count() === 1) {
35+
return cp_route('ab.experiments.edit', $this->lastCreated->id());
36+
}
37+
38+
return false;
39+
}
40+
41+
public function icon(): string
42+
{
43+
return 'copy';
44+
}
45+
46+
public static function title()
47+
{
48+
return __('Duplicate');
49+
}
50+
51+
public function visibleTo($item)
52+
{
53+
return $item instanceof Experiment;
54+
}
55+
56+
public function authorize($user, $item)
57+
{
58+
return $user->can('create a/b experiments');
59+
}
60+
}

src/Experiment/Experiment.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,6 @@ public function variants(): Collection
284284

285285
public function fresh()
286286
{
287-
return \Thoughtco\StatamicABTester\Facades\Experiment::find($this->id);
287+
return ExperimentFacade::find($this->id);
288288
}
289289
}

src/Goal/Goal.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,6 @@ public function toArray()
124124

125125
public function fresh()
126126
{
127-
return \Thoughtco\StatamicABTester\Facades\Goal::find($this->id);
127+
return GoalFacade::find($this->id);
128128
}
129129
}

tests/Controllers/ExperimentActionsControllerTest.php

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
uses(\Thoughtco\StatamicABTester\Tests\TestCase::class);
3+
uses(Thoughtco\StatamicABTester\Tests\TestCase::class);
44

55
use Statamic\Facades\User;
66
use Thoughtco\StatamicABTester\Facades\Experiment;
@@ -43,4 +43,117 @@
4343
$this->post(cp_route('ab.experiments.actions'), [])
4444
->assertSessionHasErrors(['action']);
4545
});
46+
47+
it('duplicates a manual experiment', function () {
48+
$experiment = tap(Experiment::make('original')
49+
->title('My Experiment')
50+
->type('manual')
51+
->goals(['goal-1'])
52+
->data([
53+
'manual_fields' => [
54+
['handle' => 'control', 'label' => 'Control', 'weight' => 50],
55+
['handle' => 'variant_a', 'label' => 'Variant A', 'weight' => 50],
56+
],
57+
])
58+
->published(true))
59+
->save();
60+
61+
$this->post(cp_route('ab.experiments.actions'), [
62+
'action' => 'duplicate_experiment',
63+
'selections' => [$experiment->id()],
64+
'values' => [],
65+
])->assertOk();
66+
67+
expect(Experiment::all()->count())->toBe(2);
68+
69+
$copy = Experiment::all()->reject(fn ($e) => $e->id() === $experiment->id())->first();
70+
expect($copy->title())->toBe('Copy of My Experiment');
71+
expect($copy->type())->toBe('manual');
72+
expect($copy->goals())->toBe(['goal-1']);
73+
expect($copy->published())->toBeFalse();
74+
expect($copy->completedAt())->toBeNull();
75+
expect($copy->get('manual_fields'))->toBe($experiment->get('manual_fields'));
76+
});
77+
78+
it('duplicates an item experiment preserving experiment_fields and traffic_split', function () {
79+
$experiment = tap(Experiment::make('item-exp')
80+
->title('Item Experiment')
81+
->type('item')
82+
->goals(['goal-1'])
83+
->data([
84+
'item_id' => 'abc-123',
85+
'experiment_fields' => ['fields' => ['title'], 'values' => ['title' => 'Variant Title']],
86+
'traffic_split' => 30,
87+
])
88+
->published(true))
89+
->save();
90+
91+
$this->post(cp_route('ab.experiments.actions'), [
92+
'action' => 'duplicate_experiment',
93+
'selections' => [$experiment->id()],
94+
'values' => [],
95+
])->assertOk();
96+
97+
$copy = Experiment::all()->reject(fn ($e) => $e->id() === $experiment->id())->first();
98+
expect($copy->title())->toBe('Copy of Item Experiment');
99+
expect($copy->type())->toBe('item');
100+
expect($copy->get('item_id'))->toBe('abc-123');
101+
expect($copy->get('traffic_split'))->toBe(30);
102+
expect($copy->get('experiment_fields'))->toBe($experiment->get('experiment_fields'));
103+
expect($copy->published())->toBeFalse();
104+
});
105+
106+
it('redirects to edit page when duplicating a single experiment', function () {
107+
$experiment = tap(Experiment::make('dup-redirect')
108+
->title('Redirect Test')
109+
->type('manual')
110+
->goals([]))
111+
->save();
112+
113+
$response = $this->post(cp_route('ab.experiments.actions'), [
114+
'action' => 'duplicate_experiment',
115+
'selections' => [$experiment->id()],
116+
'values' => [],
117+
])->assertOk();
118+
119+
$copyId = Experiment::all()
120+
->reject(fn ($e) => $e->id() === $experiment->id())
121+
->first()
122+
->id();
123+
124+
expect($response->json('redirect'))->toBe(cp_route('ab.experiments.edit', $copyId));
125+
});
126+
127+
it('duplicates multiple experiments in bulk without redirecting', function () {
128+
$exp1 = tap(Experiment::make('bulk-1')->title('Experiment One')->type('manual')->goals([]))->save();
129+
$exp2 = tap(Experiment::make('bulk-2')->title('Experiment Two')->type('manual')->goals([]))->save();
130+
131+
$response = $this->post(cp_route('ab.experiments.actions'), [
132+
'action' => 'duplicate_experiment',
133+
'selections' => [$exp1->id(), $exp2->id()],
134+
'values' => [],
135+
])->assertOk();
136+
137+
expect(Experiment::all()->count())->toBe(4);
138+
expect($response->json('redirect'))->toBeFalsy();
139+
});
140+
141+
it('does not copy completed_at when duplicating a completed experiment', function () {
142+
$experiment = tap(Experiment::make('completed-exp')
143+
->title('Completed Experiment')
144+
->type('manual')
145+
->goals([])
146+
->completedAt(now()))
147+
->save();
148+
149+
$this->post(cp_route('ab.experiments.actions'), [
150+
'action' => 'duplicate_experiment',
151+
'selections' => [$experiment->id()],
152+
'values' => [],
153+
])->assertOk();
154+
155+
$copy = Experiment::all()->reject(fn ($e) => $e->id() === $experiment->id())->first();
156+
expect($copy->completedAt())->toBeNull();
157+
expect($copy->published())->toBeFalse();
158+
});
46159
});

tests/Controllers/ExperimentsControllerTest.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Statamic\Facades\Entry;
99
use Statamic\Facades\User;
1010
use Thoughtco\StatamicABTester\Facades\Experiment;
11+
use Thoughtco\StatamicABTester\Models\AbTestResult;
1112

1213
beforeEach(function () {
1314
$this->actingAs(User::make()->makeSuper()->save());
@@ -248,7 +249,7 @@
248249
// to control exact counts: 200 hits each, 10% vs 20% conversion
249250
$insertRows = function (string $type, int|string $variant, int $count) use ($experiment) {
250251
for ($i = 0; $i < $count; $i++) {
251-
\Thoughtco\StatamicABTester\Models\AbTestResult::create([
252+
AbTestResult::create([
252253
'experiment_id' => $experiment->id(),
253254
'variation' => $variant,
254255
'type' => $type,
@@ -282,7 +283,7 @@
282283

283284
$insertRows = function (string $type, int|string $variant, int $count) use ($experiment) {
284285
for ($i = 0; $i < $count; $i++) {
285-
\Thoughtco\StatamicABTester\Models\AbTestResult::create([
286+
AbTestResult::create([
286287
'experiment_id' => $experiment->id(),
287288
'variation' => $variant,
288289
'type' => $type,
@@ -326,19 +327,19 @@
326327

327328
it('stores traffic_split when creating an item experiment', function () {
328329
// Create blueprint and collection for item experiment
329-
$blueprint = \Statamic\Facades\Blueprint::make('article');
330+
$blueprint = Blueprint::make('article');
330331
$blueprint->setContents([
331332
'fields' => [
332333
['handle' => 'title', 'field' => ['type' => 'text']],
333334
],
334335
])->setNamespace('collections.articles');
335336
$blueprint->save();
336337

337-
$collection = \Statamic\Facades\Collection::make('articles');
338+
$collection = Collection::make('articles');
338339
$collection->entryBlueprints(['article']);
339340
$collection->save();
340341

341-
$entry = tap(\Statamic\Facades\Entry::make()
342+
$entry = tap(Entry::make()
342343
->collection('articles')
343344
->blueprint('article')
344345
->slug('test-article')

0 commit comments

Comments
 (0)