Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .machina/flow-steps/code_list.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
export default ({ defineStep }) => [
// Fill in the code list's own label field (textarea[name="label"]).
// Uses a direct name selector to avoid ambiguity with code-row label inputs.
defineStep('I fill in the code list label with {string}', async (ctx, value) => {
await ctx.page.locator('textarea[name="label"], input[name="label"]').first().waitFor({ timeout: 10000 });
await ctx.page.locator('textarea[name="label"], input[name="label"]').first().fill(value);
}),

// Fill in the code list label with a value that includes a timestamp suffix so each
// test run creates a distinct code list (avoids uniqueness-per-instrument constraint).
defineStep('I fill in the code list label with {string} and a unique suffix', async (ctx, base) => {
const label = `${base}-${Date.now()}`;
const field = ctx.page.locator('textarea[name="label"], input[name="label"]').first();
await field.waitFor({ timeout: 10000 });
await field.fill(label);
}),

// Fill in the value and label for a specific code row (1-indexed).
// Uses react-final-form FieldArray names: codes[N].value (textarea) and codes[N].label (input).
defineStep('I fill in the code row {int} with value {string} and label {string}', async (ctx, row, value, label) => {
const idx = row - 1;
await ctx.page.locator(`textarea[name="codes[${idx}].value"]`).fill(value);
const labelInput = ctx.page.locator(`input[name="codes[${idx}].label"]`);
await labelInput.fill(label);
await labelInput.press('Tab');
}),

// Wait for the codes table to show at least one row — confirms the code list data has loaded
// after a redirect (React fetches asynchronously after the URL changes).
defineStep('I wait for the code list to load', async (ctx) => {
await ctx.page.locator('table tbody tr').first().waitFor({ timeout: 10000 });
}),

// Assert the label input value for a specific code row (1-indexed).
// Category labels render inside MUI Autocomplete inputs, not as text nodes, so we read inputValue.
defineStep('the code row {int} should have label {string}', async (ctx, row, label) => {
const input = ctx.page.locator(`input[name="codes[${row - 1}].label"]`);
await input.waitFor({ timeout: 5000 });
const actual = await input.inputValue();
if (actual !== label) throw new Error(`Expected code row ${row} label "${label}" but got "${actual}"`);
}),

// Click the add-code button (aria-label="Add code") that sits next to the "Codes" heading.
// The button only renders once instrument data has loaded, so we wait for it.
defineStep('I click the add code button', async (ctx) => {
const addBtn = ctx.page.locator('button[aria-label="Add code"]');
await addBtn.waitFor({ timeout: 10000 });
await addBtn.click();
}),
];
7 changes: 7 additions & 0 deletions .machina/flow-steps/ui.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,11 @@ export default ({ defineStep }) => [
await listbox.waitFor({ timeout: 5000 });
await listbox.locator('[role="option"]').filter({ hasText: option }).click();
}),

// Wait for the URL to match a regex pattern, then settle — needed after async Redux redirects
// where the URL changes client-side before the subsequent API fetches begin.
defineStep('I wait for the URL to match {string}', async (ctx, pattern) => {
await ctx.page.waitForURL(new RegExp(pattern), { timeout: 10000 });
await ctx.page.waitForLoadState('networkidle', { timeout: 10000 });
}),
];
2 changes: 1 addition & 1 deletion app/controllers/code_lists_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def safe_params
# and their nested categories.
codes_params = params[:codes] ? params.delete(:codes) : params[:code_list].delete(:codes)
if codes_params
codes_params.map.with_index do | code, index |
codes_params.each_with_index do | code, index |
code[:order] = index + 1 unless code[:order].present?
next if code[:value].blank? && code[:label].blank?
existing_category = @instrument.categories.find_by_label(code[:label])
Expand Down
2 changes: 1 addition & 1 deletion react/src/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ export const CodeLists = {
dispatch(redirectTo(url(routes.instruments.instrument.build.codeLists.show, { instrument_id: instrumentId, codeListId: res.data.id })));
})
.catch(err => {
dispatch(saveError('new', 'CodeList', err.response.data.error_sentence));
dispatch(saveError('new', 'CodeList', err.response?.data?.error_sentence));
});
};
},
Expand Down
14 changes: 6 additions & 8 deletions react/src/components/CodeListForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,13 @@ export const CodeListForm = (props) => {
const classes = useStyles();

const onSubmit = (values) => {
values = ObjectCheckForInitialValues(codeList, values)

if(isNil(codeList.id)){
values = ObjectCheckForInitialValues(codeList, values)
if (values.codes) {
values.codes.forEach((code, i) => { code.order = i + 1 })
}
if (isNil(codeList.id)) {
dispatch(CodeLists.create(instrumentId, values))
}else{
values.codes.map((code, i) => {
code.order = i + 1
return code
})
} else {
dispatch(CodeLists.update(instrumentId, codeList.id, values))
}
}
Expand Down
24 changes: 24 additions & 0 deletions test/controllers/code_lists_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,30 @@ class CodeListsControllerTest < ActionController::TestCase
assert_equal [3,4], [code_a.reload.order, code_b.reload.order]
end

test "should create code_list with multiple codes without client-supplied order" do
assert_difference('CodeList.count') do
post :create, format: :json, params: {
instrument_id: @instrument.id,
code_list: {
label: @code_list.label + '_multi',
codes: [
{ value: '1', label: 'Yes' },
{ value: '2', label: 'No' }
]
}
}
end

assert_response :success
json = JSON.parse(response.body)
codes = json['codes'].sort_by { |c| c['order'] }
assert_equal 2, codes.length
assert_equal 1, codes[0]['order']
assert_equal 2, codes[1]['order']
assert_equal '1', codes[0]['value']
assert_equal '2', codes[1]['value']
end

test "should destroy code_list" do
assert_difference('CodeList.count', -1) do
delete :destroy, format: :json, params: { instrument_id: @instrument.id, id: @code_list }
Expand Down
1 change: 1 addition & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

class ActiveSupport::TestCase
include FactoryBot::Syntax::Methods
fixtures :all
setup do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.start
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Feature: Create code list with multiple codes in one submit (#878)
Regression for https://github.com/CLOSER-Cohorts/archivist/issues/878
— creating a brand-new code list with two or more codes in a single
submit must succeed. The create path previously omitted order on each
code; the update path correctly assigned it.

Scenario: New code list with 2 codes saves on first submit
When I log in as "simon.reed@browsergroup.com" with password "Password123!"
And I navigate to "/instruments/mcs_18_ypsc/build/code_lists/new"
And I wait for the page to settle
And I fill in the code list label with "yesno-issue-878" and a unique suffix
And I click the add code button
And I fill in the code row 1 with value "1" and label "Yes"
And I click the add code button
And I fill in the code row 2 with value "2" and label "No"
And I click the "Save" button
And I wait for the URL to match "code_lists/\d+"
And I wait for the code list to load
Then the code row 1 should have label "Yes"
And the code row 2 should have label "No"