@outputai/llm 0.11.1-next.da26845.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -3
- package/src/agent.js +7 -2
- package/src/generate.js +1 -0
- package/src/index.d.ts +14 -6
- package/src/utils/wrap.js +35 -2
- package/src/agent.spec.js +0 -595
- package/src/ai_provider.spec.js +0 -217
- package/src/ai_sdk_options.spec.js +0 -390
- package/src/fixtures/image_response_v7_google_vertex.js +0 -72
- package/src/fixtures/image_response_v7_openai.js +0 -77
- package/src/fixtures/models_api_light.json +0 -675
- package/src/fixtures/models_api_v7_subset.json +0 -259
- package/src/fixtures/stream_response_v7_anthropic.js +0 -389
- package/src/fixtures/stream_response_v7_google_vertex.js +0 -462
- package/src/fixtures/stream_response_v7_openai.js +0 -353
- package/src/fixtures/stream_response_v7_perplexity.js +0 -671
- package/src/fixtures/text_response_v7_anthropic.js +0 -359
- package/src/fixtures/text_response_v7_anthropic_cache.js +0 -719
- package/src/fixtures/text_response_v7_google_vertex.js +0 -435
- package/src/fixtures/text_response_v7_openai.js +0 -347
- package/src/fixtures/text_response_v7_perplexity.js +0 -764
- package/src/generate.spec.js +0 -525
- package/src/prompt/content.spec.js +0 -208
- package/src/prompt/interpolations.spec.js +0 -109
- package/src/prompt/loader.full.spec.js +0 -309
- package/src/prompt/loader.spec.js +0 -258
- package/src/prompt/markup/attributes.spec.js +0 -132
- package/src/prompt/markup/nodes.spec.js +0 -333
- package/src/prompt/markup/tokenizer.spec.js +0 -149
- package/src/prompt/markup/tokens.spec.js +0 -250
- package/src/prompt/validations.spec.js +0 -812
- package/src/utils/cost.full.spec.js +0 -272
- package/src/utils/cost.spec.js +0 -290
- package/src/utils/error_handler.spec.js +0 -230
- package/src/utils/file.spec.js +0 -89
- package/src/utils/image.spec.js +0 -20
- package/src/utils/legacy_cost_attribute.spec.js +0 -278
- package/src/utils/models.spec.js +0 -119
- package/src/utils/models_pricing.spec.js +0 -162
- package/src/utils/skills.spec.js +0 -168
- package/src/utils/sources.spec.js +0 -122
- package/src/utils/stream.spec.js +0 -55
- package/src/utils/tools.spec.js +0 -167
- package/src/utils/usage.full.spec.js +0 -215
- package/src/utils/usage.spec.js +0 -246
- package/src/utils/wrap.spec.js +0 -467
- package/src/validations.spec.js +0 -631
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
|
|
3
|
-
const liquidMocks = vi.hoisted( () => ( {
|
|
4
|
-
createContext: vi.fn(),
|
|
5
|
-
options: { strictVariables: true },
|
|
6
|
-
parseAndRenderSync: vi.fn(),
|
|
7
|
-
registerFilter: vi.fn()
|
|
8
|
-
} ) );
|
|
9
|
-
|
|
10
|
-
vi.mock( 'liquidjs', () => ( {
|
|
11
|
-
Context: class Context {
|
|
12
|
-
constructor( ...args ) {
|
|
13
|
-
liquidMocks.createContext( this, ...args );
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
Liquid: class Liquid {
|
|
17
|
-
options = liquidMocks.options;
|
|
18
|
-
parseAndRenderSync = liquidMocks.parseAndRenderSync;
|
|
19
|
-
registerFilter = liquidMocks.registerFilter;
|
|
20
|
-
}
|
|
21
|
-
} ) );
|
|
22
|
-
|
|
23
|
-
vi.mock( 'gray-matter', () => ( {
|
|
24
|
-
default: vi.fn()
|
|
25
|
-
} ) );
|
|
26
|
-
|
|
27
|
-
vi.mock( '@outputai/core/sdk/helpers', () => ( {
|
|
28
|
-
Path: {
|
|
29
|
-
resolveInvocationDir: vi.fn( () => '/invocation' )
|
|
30
|
-
}
|
|
31
|
-
} ) );
|
|
32
|
-
|
|
33
|
-
vi.mock( './interpolations.js', () => ( {
|
|
34
|
-
pipeInterpolations: vi.fn( value => value ),
|
|
35
|
-
interpolationFilterToken: '__var_safe',
|
|
36
|
-
encode: vi.fn(),
|
|
37
|
-
decode: vi.fn( value => value )
|
|
38
|
-
} ) );
|
|
39
|
-
|
|
40
|
-
vi.mock( '../utils/file.js', () => ( {
|
|
41
|
-
searchAndReadFile: vi.fn()
|
|
42
|
-
} ) );
|
|
43
|
-
|
|
44
|
-
vi.mock( './content.js', () => ( {
|
|
45
|
-
parseContent: vi.fn()
|
|
46
|
-
} ) );
|
|
47
|
-
|
|
48
|
-
vi.mock( './validations.js', () => ( {
|
|
49
|
-
parsePromptSchema: vi.fn( prompt => prompt )
|
|
50
|
-
} ) );
|
|
51
|
-
|
|
52
|
-
import { loadPrompt } from './loader.js';
|
|
53
|
-
import { Path } from '@outputai/core/sdk/helpers';
|
|
54
|
-
import matter from 'gray-matter';
|
|
55
|
-
import { decode, pipeInterpolations } from './interpolations.js';
|
|
56
|
-
import { searchAndReadFile } from '../utils/file.js';
|
|
57
|
-
import { parseContent } from './content.js';
|
|
58
|
-
import { parsePromptSchema } from './validations.js';
|
|
59
|
-
|
|
60
|
-
const defaultConfig = {
|
|
61
|
-
provider: 'anthropic',
|
|
62
|
-
model: 'claude-3-5-sonnet-20241022'
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const defaultParsed = {
|
|
66
|
-
messages: [ { role: 'user', content: 'Hi' } ],
|
|
67
|
-
instructions: null
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const stubMatter = ( config = defaultConfig ) => {
|
|
71
|
-
matter.mockImplementation( ( _input, options ) => {
|
|
72
|
-
if ( options?.engines ) {
|
|
73
|
-
return { matter: 'RAW_YAML', content: 'RAW_BODY' };
|
|
74
|
-
}
|
|
75
|
-
return { data: { ...config } };
|
|
76
|
-
} );
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
describe( 'loadPrompt', () => {
|
|
80
|
-
beforeEach( () => {
|
|
81
|
-
vi.clearAllMocks();
|
|
82
|
-
Path.resolveInvocationDir.mockReturnValue( '/invocation' );
|
|
83
|
-
searchAndReadFile.mockReturnValue( { content: 'FILE', dir: '/mock/dir' } );
|
|
84
|
-
pipeInterpolations.mockImplementation( value => value );
|
|
85
|
-
decode.mockImplementation( value => value );
|
|
86
|
-
liquidMocks.parseAndRenderSync.mockImplementation( template => template );
|
|
87
|
-
parseContent.mockReturnValue( { ...defaultParsed, messages: [ ...defaultParsed.messages ] } );
|
|
88
|
-
parsePromptSchema.mockImplementation( prompt => prompt );
|
|
89
|
-
stubMatter();
|
|
90
|
-
} );
|
|
91
|
-
|
|
92
|
-
afterEach( () => {
|
|
93
|
-
vi.restoreAllMocks();
|
|
94
|
-
} );
|
|
95
|
-
|
|
96
|
-
it( 'splits, renders, parses, and validates the prompt', () => {
|
|
97
|
-
const variables = { name: 'World' };
|
|
98
|
-
|
|
99
|
-
const result = loadPrompt( 'test', variables );
|
|
100
|
-
|
|
101
|
-
expect( searchAndReadFile ).toHaveBeenCalledWith( '/invocation', 'test.prompt' );
|
|
102
|
-
expect( matter ).toHaveBeenNthCalledWith( 1, 'FILE', expect.objectContaining( {
|
|
103
|
-
engines: { yaml: expect.any( Function ) }
|
|
104
|
-
} ) );
|
|
105
|
-
expect( matter ).toHaveBeenNthCalledWith( 2, '---\nRAW_YAML\n---\n' );
|
|
106
|
-
expect( decode ).toHaveBeenCalledWith( defaultConfig );
|
|
107
|
-
expect( parseContent ).toHaveBeenCalledWith( 'RAW_BODY', undefined );
|
|
108
|
-
expect( parsePromptSchema ).toHaveBeenCalledWith( {
|
|
109
|
-
name: 'test',
|
|
110
|
-
config: defaultConfig,
|
|
111
|
-
messages: defaultParsed.messages,
|
|
112
|
-
instructions: null,
|
|
113
|
-
fileDir: '/mock/dir',
|
|
114
|
-
variables
|
|
115
|
-
} );
|
|
116
|
-
expect( result ).toEqual( parsePromptSchema.mock.calls[0][0] );
|
|
117
|
-
} );
|
|
118
|
-
|
|
119
|
-
it( 'renders frontmatter before content using one Liquid context', () => {
|
|
120
|
-
const variables = { name: 'World' };
|
|
121
|
-
|
|
122
|
-
loadPrompt( 'test', variables );
|
|
123
|
-
|
|
124
|
-
const context = liquidMocks.createContext.mock.calls[0][0];
|
|
125
|
-
expect( liquidMocks.createContext ).toHaveBeenCalledWith( context, variables, liquidMocks.options, { sync: true } );
|
|
126
|
-
expect( pipeInterpolations ).toHaveBeenNthCalledWith( 1, 'RAW_YAML' );
|
|
127
|
-
expect( pipeInterpolations ).toHaveBeenNthCalledWith( 2, 'RAW_BODY' );
|
|
128
|
-
expect( liquidMocks.parseAndRenderSync ).toHaveBeenNthCalledWith( 1, 'RAW_YAML', context );
|
|
129
|
-
expect( liquidMocks.parseAndRenderSync ).toHaveBeenNthCalledWith( 2, 'RAW_BODY', context );
|
|
130
|
-
} );
|
|
131
|
-
|
|
132
|
-
it( 'uses decoded frontmatter for content parsing and validation', () => {
|
|
133
|
-
const encodedConfig = {
|
|
134
|
-
...defaultConfig,
|
|
135
|
-
messageOptions: { cached: { openai: { label: 'R&D' } } }
|
|
136
|
-
};
|
|
137
|
-
const decodedConfig = {
|
|
138
|
-
...defaultConfig,
|
|
139
|
-
messageOptions: { cached: { openai: { label: 'R&D' } } }
|
|
140
|
-
};
|
|
141
|
-
stubMatter( encodedConfig );
|
|
142
|
-
decode.mockReturnValue( decodedConfig );
|
|
143
|
-
|
|
144
|
-
loadPrompt( 'test' );
|
|
145
|
-
|
|
146
|
-
expect( decode ).toHaveBeenCalledWith( encodedConfig );
|
|
147
|
-
expect( parseContent ).toHaveBeenCalledWith( 'RAW_BODY', decodedConfig.messageOptions );
|
|
148
|
-
expect( parsePromptSchema ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
149
|
-
config: decodedConfig
|
|
150
|
-
} ) );
|
|
151
|
-
} );
|
|
152
|
-
|
|
153
|
-
it( 'passes the provided prompt directory to searchAndReadFile', () => {
|
|
154
|
-
loadPrompt( 'test', {}, '/custom/prompts' );
|
|
155
|
-
|
|
156
|
-
expect( searchAndReadFile ).toHaveBeenCalledWith( '/custom/prompts', 'test.prompt' );
|
|
157
|
-
expect( Path.resolveInvocationDir ).not.toHaveBeenCalled();
|
|
158
|
-
} );
|
|
159
|
-
|
|
160
|
-
it( 'renders the piped template, not the raw body', () => {
|
|
161
|
-
pipeInterpolations.mockReturnValue( 'PIPED' );
|
|
162
|
-
|
|
163
|
-
loadPrompt( 'test', { name: 'World' } );
|
|
164
|
-
|
|
165
|
-
expect( liquidMocks.parseAndRenderSync ).toHaveBeenNthCalledWith( 2, 'PIPED', expect.any( Object ) );
|
|
166
|
-
expect( parseContent ).toHaveBeenCalledWith( 'PIPED', undefined );
|
|
167
|
-
} );
|
|
168
|
-
|
|
169
|
-
it( 'passes config.messageOptions to parseContent', () => {
|
|
170
|
-
const messageOptions = { cached: { anthropic: { cacheControl: { type: 'ephemeral' } } } };
|
|
171
|
-
stubMatter( { ...defaultConfig, messageOptions } );
|
|
172
|
-
|
|
173
|
-
loadPrompt( 'test' );
|
|
174
|
-
|
|
175
|
-
expect( parseContent ).toHaveBeenCalledWith( 'RAW_BODY', messageOptions );
|
|
176
|
-
} );
|
|
177
|
-
|
|
178
|
-
it( 'uses parseContent for messages and instructions', () => {
|
|
179
|
-
parseContent.mockReturnValue( {
|
|
180
|
-
messages: [],
|
|
181
|
-
instructions: 'Generate a poster.'
|
|
182
|
-
} );
|
|
183
|
-
|
|
184
|
-
const result = loadPrompt( 'image_prompt' );
|
|
185
|
-
|
|
186
|
-
expect( result.messages ).toEqual( [] );
|
|
187
|
-
expect( result.instructions ).toBe( 'Generate a poster.' );
|
|
188
|
-
} );
|
|
189
|
-
|
|
190
|
-
it( 'throws when the prompt file is not found', () => {
|
|
191
|
-
searchAndReadFile.mockReturnValue( null );
|
|
192
|
-
|
|
193
|
-
expect( () => loadPrompt( 'nonexistent' ) ).toThrow( /Prompt file "nonexistent" not found/ );
|
|
194
|
-
} );
|
|
195
|
-
|
|
196
|
-
it( 'throws when rendered content is empty', () => {
|
|
197
|
-
liquidMocks.parseAndRenderSync
|
|
198
|
-
.mockReturnValueOnce( 'RAW_YAML' )
|
|
199
|
-
.mockReturnValueOnce( ' ' );
|
|
200
|
-
|
|
201
|
-
expect( () => loadPrompt( 'empty_prompt' ) ).toThrow( /Prompt "empty_prompt" has no content/ );
|
|
202
|
-
} );
|
|
203
|
-
|
|
204
|
-
it( 'wraps split errors with the prompt name', () => {
|
|
205
|
-
matter.mockImplementation( () => {
|
|
206
|
-
throw new Error( 'bad delimiters' );
|
|
207
|
-
} );
|
|
208
|
-
|
|
209
|
-
expect( () => loadPrompt( 'writer@v1' ) ).toThrow(
|
|
210
|
-
/Error parsing frontmatter on prompt "writer@v1": bad delimiters/
|
|
211
|
-
);
|
|
212
|
-
} );
|
|
213
|
-
|
|
214
|
-
it( 'wraps content render errors with the prompt name', () => {
|
|
215
|
-
liquidMocks.parseAndRenderSync
|
|
216
|
-
.mockReturnValueOnce( 'RAW_YAML' )
|
|
217
|
-
.mockImplementationOnce( () => {
|
|
218
|
-
throw new Error( 'unknown variable' );
|
|
219
|
-
} );
|
|
220
|
-
|
|
221
|
-
expect( () => loadPrompt( 'writer@v1' ) ).toThrow(
|
|
222
|
-
/Error rendering content on prompt "writer@v1": unknown variable/
|
|
223
|
-
);
|
|
224
|
-
} );
|
|
225
|
-
|
|
226
|
-
it( 'wraps frontmatter render errors with the prompt name', () => {
|
|
227
|
-
liquidMocks.parseAndRenderSync.mockImplementationOnce( () => {
|
|
228
|
-
throw new Error( 'unknown filter' );
|
|
229
|
-
} );
|
|
230
|
-
|
|
231
|
-
expect( () => loadPrompt( 'writer@v1' ) ).toThrow(
|
|
232
|
-
/Error rendering frontmatter on prompt "writer@v1": unknown filter/
|
|
233
|
-
);
|
|
234
|
-
} );
|
|
235
|
-
|
|
236
|
-
it( 'wraps frontmatter yaml parse errors with the prompt name', () => {
|
|
237
|
-
matter.mockImplementation( ( _input, options ) => {
|
|
238
|
-
if ( options?.engines ) {
|
|
239
|
-
return { matter: 'RAW_YAML', content: 'RAW_BODY' };
|
|
240
|
-
}
|
|
241
|
-
throw new Error( 'invalid yaml' );
|
|
242
|
-
} );
|
|
243
|
-
|
|
244
|
-
expect( () => loadPrompt( 'writer@v1' ) ).toThrow(
|
|
245
|
-
/Error converting frontmatter yaml to js on prompt "writer@v1": invalid yaml/
|
|
246
|
-
);
|
|
247
|
-
} );
|
|
248
|
-
|
|
249
|
-
it( 'wraps content parse errors with the prompt name', () => {
|
|
250
|
-
parseContent.mockImplementation( () => {
|
|
251
|
-
throw new Error( 'unknown option' );
|
|
252
|
-
} );
|
|
253
|
-
|
|
254
|
-
expect( () => loadPrompt( 'writer@v1' ) ).toThrow(
|
|
255
|
-
/Error parsing content on prompt "writer@v1": unknown option/
|
|
256
|
-
);
|
|
257
|
-
} );
|
|
258
|
-
} );
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { extractAttributes } from './attributes.js';
|
|
3
|
-
|
|
4
|
-
describe( 'extractAttributes', () => {
|
|
5
|
-
it( 'returns no attributes for empty input', () => {
|
|
6
|
-
expect( extractAttributes( '' ) ).toEqual( {} );
|
|
7
|
-
} );
|
|
8
|
-
|
|
9
|
-
it( 'returns no attributes for whitespace-only input', () => {
|
|
10
|
-
expect( extractAttributes( ' \n\t ' ) ).toEqual( {} );
|
|
11
|
-
} );
|
|
12
|
-
|
|
13
|
-
it( 'parses bare attributes as true', () => {
|
|
14
|
-
expect( extractAttributes( 'cached pinned' ) ).toEqual( {
|
|
15
|
-
cached: true,
|
|
16
|
-
pinned: true
|
|
17
|
-
} );
|
|
18
|
-
} );
|
|
19
|
-
|
|
20
|
-
it( 'lowercases attribute names', () => {
|
|
21
|
-
expect( extractAttributes( 'OPTIONS=cached PINNED' ) ).toEqual( {
|
|
22
|
-
options: 'cached',
|
|
23
|
-
pinned: true
|
|
24
|
-
} );
|
|
25
|
-
} );
|
|
26
|
-
|
|
27
|
-
it( 'preserves unquoted values as strings', () => {
|
|
28
|
-
expect( extractAttributes( 'enabled=false count=001 ratio=1.25' ) ).toEqual( {
|
|
29
|
-
enabled: 'false',
|
|
30
|
-
count: '001',
|
|
31
|
-
ratio: '1.25'
|
|
32
|
-
} );
|
|
33
|
-
} );
|
|
34
|
-
|
|
35
|
-
it( 'removes matching double quotes', () => {
|
|
36
|
-
expect( extractAttributes( 'options="cached shared"' ) ).toEqual( {
|
|
37
|
-
options: 'cached shared'
|
|
38
|
-
} );
|
|
39
|
-
} );
|
|
40
|
-
|
|
41
|
-
it( 'removes matching single quotes', () => {
|
|
42
|
-
expect( extractAttributes( 'options=\'cached shared\'' ) ).toEqual( {
|
|
43
|
-
options: 'cached shared'
|
|
44
|
-
} );
|
|
45
|
-
} );
|
|
46
|
-
|
|
47
|
-
it( 'allows the opposite quote type inside quoted values', () => {
|
|
48
|
-
expect( extractAttributes( 'single="it\'s ready" double=\'say "ready"\'' ) ).toEqual( {
|
|
49
|
-
single: 'it\'s ready',
|
|
50
|
-
double: 'say "ready"'
|
|
51
|
-
} );
|
|
52
|
-
} );
|
|
53
|
-
|
|
54
|
-
it( 'preserves equals and greater-than characters inside quoted values', () => {
|
|
55
|
-
expect( extractAttributes( 'condition="score >= 10" expression=\'a = b\'' ) ).toEqual( {
|
|
56
|
-
condition: 'score >= 10',
|
|
57
|
-
expression: 'a = b'
|
|
58
|
-
} );
|
|
59
|
-
} );
|
|
60
|
-
|
|
61
|
-
it( 'supports whitespace around equals signs', () => {
|
|
62
|
-
expect( extractAttributes( 'options = "cached shared" mode = fast pinned' ) ).toEqual( {
|
|
63
|
-
options: 'cached shared',
|
|
64
|
-
mode: 'fast',
|
|
65
|
-
pinned: true
|
|
66
|
-
} );
|
|
67
|
-
} );
|
|
68
|
-
|
|
69
|
-
it( 'supports tabs and newlines between attributes', () => {
|
|
70
|
-
expect( extractAttributes( 'options="cached"\nmode=fast\tpinned' ) ).toEqual( {
|
|
71
|
-
options: 'cached',
|
|
72
|
-
mode: 'fast',
|
|
73
|
-
pinned: true
|
|
74
|
-
} );
|
|
75
|
-
} );
|
|
76
|
-
|
|
77
|
-
it( 'ignores surrounding whitespace', () => {
|
|
78
|
-
expect( extractAttributes( ' options = "cached" pinned ' ) ).toEqual( {
|
|
79
|
-
options: 'cached',
|
|
80
|
-
pinned: true
|
|
81
|
-
} );
|
|
82
|
-
} );
|
|
83
|
-
|
|
84
|
-
it( 'parses an explicitly empty value', () => {
|
|
85
|
-
expect( extractAttributes( 'options=' ) ).toEqual( { options: '' } );
|
|
86
|
-
} );
|
|
87
|
-
|
|
88
|
-
it( 'uses the last value when an attribute is repeated', () => {
|
|
89
|
-
expect( extractAttributes( 'mode=slow mode=fast' ) ).toEqual( { mode: 'fast' } );
|
|
90
|
-
} );
|
|
91
|
-
|
|
92
|
-
it( 'rejects an unterminated single-quoted value', () => {
|
|
93
|
-
expect( () => extractAttributes( 'color=\'red' ) )
|
|
94
|
-
.toThrow( /^Invalid attribute "color": quoted value is missing its closing quote \('\)\.$/ );
|
|
95
|
-
} );
|
|
96
|
-
|
|
97
|
-
it( 'rejects an unterminated double-quoted value', () => {
|
|
98
|
-
expect( () => extractAttributes( 'color="red' ) )
|
|
99
|
-
.toThrow( /^Invalid attribute "color": quoted value is missing its closing quote \("\)\.$/ );
|
|
100
|
-
} );
|
|
101
|
-
|
|
102
|
-
it( 'rejects values closed by a different quote type', () => {
|
|
103
|
-
expect( () => extractAttributes( 'color=\'red"' ) ).toThrow();
|
|
104
|
-
expect( () => extractAttributes( 'color="red\'' ) ).toThrow();
|
|
105
|
-
} );
|
|
106
|
-
|
|
107
|
-
it( 'rejects stray quotes in unquoted values', () => {
|
|
108
|
-
expect( () => extractAttributes( 'color=red\'' ) ).toThrow();
|
|
109
|
-
expect( () => extractAttributes( 'color=red"' ) ).toThrow();
|
|
110
|
-
expect( () => extractAttributes( 'color=arby\'s' ) ).toThrow();
|
|
111
|
-
expect( () => extractAttributes( 'color=jack"madman"silva' ) ).toThrow();
|
|
112
|
-
} );
|
|
113
|
-
|
|
114
|
-
it( 'rejects matching quote characters inside quoted values', () => {
|
|
115
|
-
expect( () => extractAttributes( 'message="say "hello""' ) )
|
|
116
|
-
.toThrow( /^Invalid attribute "message": quoted value contains an unexpected quote \("\)\.$/ );
|
|
117
|
-
expect( () => extractAttributes( 'message=\'say \'hello\'\'' ) ).toThrow();
|
|
118
|
-
expect( () => extractAttributes( 'color="red"dark"' ) ).toThrow();
|
|
119
|
-
expect( () => extractAttributes( 'color=\'red\'dark\'' ) ).toThrow();
|
|
120
|
-
} );
|
|
121
|
-
|
|
122
|
-
it( 'rejects an empty attribute name', () => {
|
|
123
|
-
expect( () => extractAttributes( '=value' ) )
|
|
124
|
-
.toThrow( /^Invalid attribute "=value": name is missing\.$/ );
|
|
125
|
-
} );
|
|
126
|
-
|
|
127
|
-
it( 'rejects invalid valued and bare attribute names', () => {
|
|
128
|
-
expect( () => extractAttributes( 'bad/name=value' ) )
|
|
129
|
-
.toThrow( /^Invalid attribute name "bad\/name"\.$/ );
|
|
130
|
-
expect( () => extractAttributes( 'bad/name' ) ).toThrow();
|
|
131
|
-
} );
|
|
132
|
-
} );
|