create(); $schema = FormSchema::factory()->create(['organisation_id' => $org->id]); $field = FormField::factory() ->withOptions(['a', 'b']) ->create([ 'form_schema_id' => $schema->id, 'field_type' => FormFieldType::SELECT->value, 'slug' => 'colour', 'label' => 'Colour', ]); // Suppress prior activity (factory creation) and re-bound the // window for assertion clarity. Activity::query()->delete(); app(FormFieldService::class)->update($field, [ 'options' => [ ['value' => 'a', 'label' => 'A', 'sort_order' => 0], ['value' => 'b', 'label' => 'b', 'sort_order' => 1], ['value' => 'c', 'label' => 'c', 'sort_order' => 2], ], ]); $event = Activity::query() ->where('subject_type', 'form_field') ->where('subject_id', $field->id) ->where('description', 'field.updated') ->first(); $this->assertNotNull($event); $payload = $event->properties->toArray(); $this->assertArrayHasKey('options', $payload['old']); $this->assertArrayHasKey('options', $payload['new']); // RFC-WS-6 session 2.7: activity log properties are canonicalized // at write; assertSame on canonical encodings of both sides is // byte-stable across MySQL JSON-column round-trip. $this->assertSame( JsonCanonicalizer::encode([ ['value' => 'a', 'label' => 'a', 'sort_order' => 0], ['value' => 'b', 'label' => 'b', 'sort_order' => 1], ]), JsonCanonicalizer::encode($payload['old']['options']), ); $this->assertSame( JsonCanonicalizer::encode([ ['value' => 'a', 'label' => 'A', 'sort_order' => 0], ['value' => 'b', 'label' => 'b', 'sort_order' => 1], ['value' => 'c', 'label' => 'c', 'sort_order' => 2], ]), JsonCanonicalizer::encode($payload['new']['options']), ); } public function test_field_updated_payload_omits_options_key_when_only_label_changed(): void { $org = Organisation::factory()->create(); $schema = FormSchema::factory()->create(['organisation_id' => $org->id]); $field = FormField::factory() ->withOptions(['a', 'b']) ->create([ 'form_schema_id' => $schema->id, 'field_type' => FormFieldType::SELECT->value, 'slug' => 'choice', 'label' => 'Old', ]); Activity::query()->delete(); app(FormFieldService::class)->update($field, [ 'label' => 'New', ]); $event = Activity::query() ->where('subject_type', 'form_field') ->where('subject_id', $field->id) ->where('description', 'field.updated') ->first(); $this->assertNotNull($event); $payload = $event->properties->toArray(); $this->assertArrayNotHasKey('options', $payload['old']); $this->assertArrayNotHasKey('options', $payload['new']); } public function test_options_replaced_emits_on_form_field_subject(): void { $org = Organisation::factory()->create(); $schema = FormSchema::factory()->create(['organisation_id' => $org->id]); $field = FormField::factory()->create([ 'form_schema_id' => $schema->id, 'field_type' => FormFieldType::SELECT->value, ]); Activity::query()->delete(); app(FormFieldOptionService::class)->replaceOptions($field, [ ['value' => 'x', 'label' => 'X', 'sort_order' => 0], ]); $this->assertNotNull(Activity::query() ->where('subject_type', 'form_field') ->where('subject_id', $field->id) ->where('description', 'field.options_replaced') ->first()); } public function test_options_replaced_silent_on_library_subject(): void { $org = Organisation::factory()->create(); $library = FormFieldLibrary::factory()->create(['organisation_id' => $org->id]); Activity::query()->delete(); app(FormFieldOptionService::class)->replaceOptions($library, [ ['value' => 'x', 'label' => 'X', 'sort_order' => 0], ]); $this->assertNull(Activity::query() ->where('subject_type', 'form_field_library') ->where('description', 'field.options_replaced') ->first()); } }