Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for external secondary instances #259

Merged
merged 27 commits into from
Dec 11, 2024
Merged
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7ae25f3
scenario: correct porting mistake in repeat-based secondary instance …
eyelidlessness Nov 19, 2024
c57cb10
Set up shared access to form attachment fixtures
eyelidlessness Nov 21, 2024
ee8cb7f
Reuse simpler glob import generalization for XML XForms fixtures
eyelidlessness Nov 25, 2024
3d58132
Shared abstractions for jr resources
eyelidlessness Nov 21, 2024
4fa77ba
Former JR port ReferenceManagerTestUtils now references SharedJRResou…
eyelidlessness Nov 21, 2024
9b5c195
engine: separate configuration for retrieval of form definition, atta…
eyelidlessness Nov 21, 2024
b3f7b9c
scenario: support for JRResourceService to handle `fetchFormAttachments`
eyelidlessness Nov 25, 2024
7b59a0b
scenario: add failing tests for basic external secondary instance sup…
eyelidlessness Nov 25, 2024
75bc335
engine/client: expand `FetchResourceResponse` to include optional `he…
eyelidlessness Nov 25, 2024
0fbc3c5
engine: support for XML external secondary instances
eyelidlessness Nov 25, 2024
edf14f6
web-forms (Vue UI): auto setup JRResourceService to serve/load form a…
eyelidlessness Nov 25, 2024
e14ea93
engine: special case 404 response as a “blank” secondary instance
eyelidlessness Nov 26, 2024
2b65998
engine: support CSV external secondary instances
eyelidlessness Nov 26, 2024
d898240
scenario: remove now-impertinent notes on CSV not found test
eyelidlessness Nov 26, 2024
706aa50
scenario: remove now-impertinent notes about header-only CSV test
eyelidlessness Nov 26, 2024
4e68c48
engine: support for GeoJSON external secondary instances
eyelidlessness Nov 26, 2024
3e39af6
scenario: test exercising GeoJSON external secondary instances now pa…
eyelidlessness Nov 26, 2024
3b91a2c
scenario: test basic CSV external secondary instance support
eyelidlessness Nov 26, 2024
c0dc32c
fix: parsing CSV with trailing new lines
eyelidlessness Nov 26, 2024
2fddcec
changeset
eyelidlessness Nov 26, 2024
6b1f145
scenario: test details of CSV parsing…
eyelidlessness Nov 27, 2024
7222c30
engine: missing resource behavior error by default, blank by config
eyelidlessness Nov 27, 2024
4bcff2b
engine: client constant **types** are exported without the `constants…
eyelidlessness Nov 27, 2024
6f107bc
web-forms: partial support for previewing forms w/ external secondary…
eyelidlessness Nov 27, 2024
bd4a064
Use `JR_RESOURCE_URL_PROTOCOL` constant rather than same value inline
eyelidlessness Dec 11, 2024
d5ec9bd
Remove `fetchResource` config, update remaining references to more sp…
eyelidlessness Dec 11, 2024
128e15f
scenario: remove redundant tests with inline external secondary insta…
eyelidlessness Dec 11, 2024
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
232 changes: 232 additions & 0 deletions packages/scenario/test/secondary-instances.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { xformAttachmentFixturesByDirectory } from '@getodk/common/fixtures/xform-attachments.ts';
import { JRResourceService } from '@getodk/common/jr-resources/JRResourceService.ts';
import { getBlobText } from '@getodk/common/lib/web-compat/blob.ts';
import {
bind,
body,
Expand All @@ -16,6 +17,7 @@ import {
t,
title,
} from '@getodk/common/test/fixtures/xform-dsl/index.ts';
import type { PartiallyKnownString } from '@getodk/common/types/string/PartiallyKnownString.ts';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { stringAnswer } from '../src/answer/ExpectedStringAnswer.ts';
import { Scenario } from '../src/jr/Scenario.ts';
Expand Down Expand Up @@ -823,4 +825,234 @@ describe('Secondary instances', () => {
expect(scenario.answerOf('/data/second')).toEqualAnswer(stringAnswer('z'));
});
});

describe('CSV parsing', () => {
const BOM = '\ufeff';
type BOM = typeof BOM;

// prettier-ignore
type ColumnDelimiter =
| ','
| ';'
| '\t'
| '|';

// prettier-ignore
type RowDelimiter =
| '\n'
| '\r'
| '\r\n';

type ExpectedFailure = 'parse' | 'select-value';

interface CSVCase {
readonly description: string;

/** @default ',' */
readonly columnDelimiter?: PartiallyKnownString<ColumnDelimiter>;

/** @default '\n' */
readonly rowDelimiter?: PartiallyKnownString<RowDelimiter>;

/** @default '' */
readonly bom?: BOM | '';

/** @default 0 */
readonly columnPadding?: number;

/** @default null */
readonly expectedFailure?: ExpectedFailure | null;

/** @default null */
readonly surprisingSuccessWarning?: string | null;
}

const csvCases: readonly CSVCase[] = [
{
description: 'BOM is not treated as part of first column header',
bom: BOM,
},
{
description: 'column delimiter: semicolon',
columnDelimiter: ';',
},
{
description: 'column delimiter: tab',
columnDelimiter: '\t',
},
{
description: 'column delimiter: pipe',
columnDelimiter: '|',
},
{
description: 'unsupported column delimiter: $',
columnDelimiter: '$',
expectedFailure: 'parse',
},
{
description: 'row delimiter: LF',
rowDelimiter: '\n',
},
{
description: 'row delimiter: CR',
rowDelimiter: '\r',
},
{
description: 'row delimiter: CRLF',
rowDelimiter: '\r\n',
},
{
description: 'unsupported row delimiter: LFLF',
rowDelimiter: `\n\n`,
expectedFailure: 'parse',
},

{
description: 'somewhat surprisingly supported row delimiter: LFCR',
lognaturel marked this conversation as resolved.
Show resolved Hide resolved
rowDelimiter: `\n\r`,
surprisingSuccessWarning:
"LFCR is not an expected line separator in any known-common usage. It's surprising that Papaparse does not fail parsing this case, at least parsing rows!",
},

{
description: 'whitespace padding around column delimiter is not ignored (by default)',
columnDelimiter: ',',
columnPadding: 1,
expectedFailure: 'select-value',
},
];

// Note: this isn't set up with `describe.each` because it would create a superfluous outer description where the inner description must be applied with `it` (to perform async setup)
csvCases.forEach(
({
description,
columnDelimiter = ',',
rowDelimiter = '\n',
bom = '',
columnPadding = 0,
expectedFailure = null,
surprisingSuccessWarning = null,
}) => {
const LOWER_ALPHA_ASCII_LETTER_COUNT = 26;
const lowerAlphaASCIILetters = Array.from(
{
length: LOWER_ALPHA_ASCII_LETTER_COUNT,
},
(_, i) => {
return String.fromCharCode(i + 97);
}
);

type CSVRow = readonly [itemLabel: string, itemValue: string];

const rows: readonly CSVRow[] = [
['item-label', 'item-value'],

...lowerAlphaASCIILetters.map((letter): CSVRow => {
return [letter.toUpperCase(), letter];
}),
];
const baseCSVFixture = rows
.map((row) => {
const padding = ' '.repeat(columnPadding);
const delimiter = `${padding}${columnDelimiter}${padding}`;

return row.join(delimiter);
})
.join(rowDelimiter);

const csvAttachmentFileName = 'csv-attachment.csv';
const csvAttachmentURL = `jr://file/${csvAttachmentFileName}` as const;
const formTitle = 'External secondary instance (CSV)';
const formDefinition = html(
head(
title(formTitle),
model(
// prettier-ignore
mainInstance(
t('data id="external-secondary-instance-csv"',
t('letter'))),

t(`instance id="external-csv" src="${csvAttachmentURL}"`),

bind('/data/letter').type('string')
)
),
body(
select1Dynamic(
'/data/letter',
"instance('external-csv')/root/item",
'item-value',
'item-label'
)
)
);

let resourceService: JRResourceService;

beforeEach(() => {
resourceService = new JRResourceService();
});

afterEach(() => {
resourceService.reset();
});

it(description, async () => {
let csvFixture: string;

if (bom === '') {
csvFixture = baseCSVFixture;
} else {
const blob = new Blob([bom, baseCSVFixture]);

csvFixture = await getBlobText(blob);
}

resourceService.activateResource(
{
url: csvAttachmentURL,
fileName: csvAttachmentFileName,
mimeType: 'text/csv',
},
csvFixture
);

const letterIndex = Math.floor(Math.random() * LOWER_ALPHA_ASCII_LETTER_COUNT);
const letter = lowerAlphaASCIILetters[letterIndex]!;

const initScenario = async (): Promise<Scenario> => {
return await Scenario.init(formTitle, formDefinition, {
resourceService,
});
};

if (expectedFailure === 'parse') {
const initParseFailure = async () => {
await initScenario();
};

await expect(initParseFailure).rejects.toThrowError();

return;
}

if (surprisingSuccessWarning != null) {
// eslint-disable-next-line no-console
console.warn(surprisingSuccessWarning);
}

const scenario = await initScenario();

scenario.answer('/data/letter', letter);

if (expectedFailure === 'select-value') {
expect(scenario.answerOf('/data/letter')).toEqualAnswer(stringAnswer(''));
} else {
expect(scenario.answerOf('/data/letter')).toEqualAnswer(stringAnswer(letter));
}
});
}
);
});
});
Loading