diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..00b2dfc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,109 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + ci: + name: CI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Dart + uses: dart-lang/setup-dart@v1 + with: + sdk: stable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Get dependencies + run: | + cd packages/dart_node_core && dart pub get + cd ../dart_node_express && dart pub get + cd ../dart_node_react && dart pub get + cd ../dart_node_react_native && dart pub get + cd ../dart_node_ws && dart pub get + cd ../../examples/shared && dart pub get + cd ../backend && dart pub get + cd ../frontend && dart pub get + cd ../mobile && dart pub get + cd ../../tools/build && dart pub get + + - name: Check formatting + run: | + dart format --set-exit-if-changed packages/dart_node_core + dart format --set-exit-if-changed packages/dart_node_express + dart format --set-exit-if-changed packages/dart_node_react + dart format --set-exit-if-changed packages/dart_node_react_native + dart format --set-exit-if-changed packages/dart_node_ws + dart format --set-exit-if-changed examples/backend + dart format --set-exit-if-changed examples/frontend + dart format --set-exit-if-changed examples/mobile + dart format --set-exit-if-changed examples/shared + dart format --set-exit-if-changed tools/build + + - name: Analyze + run: | + cd packages/dart_node_core && dart analyze --no-fatal-warnings + cd ../dart_node_express && dart analyze --no-fatal-warnings + cd ../dart_node_react && dart analyze --no-fatal-warnings + cd ../dart_node_react_native && dart analyze --no-fatal-warnings + cd ../dart_node_ws && dart analyze --no-fatal-warnings + cd ../../examples/backend && dart analyze --no-fatal-warnings + cd ../frontend && dart analyze --no-fatal-warnings + cd ../mobile && dart analyze --no-fatal-warnings + cd ../shared && dart analyze --no-fatal-warnings + cd ../../tools/build && dart analyze --no-fatal-warnings + + - name: Install Node dependencies + working-directory: examples/backend + run: npm install + + - name: Build backend + run: dart run tools/build/build.dart backend + + - name: Test backend + working-directory: examples/backend + run: dart test + + - name: Test frontend + working-directory: examples/frontend + run: dart test -p chrome + + - name: Test mobile with coverage + working-directory: examples/mobile + run: | + dart test --coverage=coverage + dart pub global activate coverage + dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --report-on=lib + COVERAGE=$(awk -F: '/^LF:/ { total += $2 } /^LH:/ { covered += $2 } END { if (total > 0) printf "%.1f", (covered / total) * 100; else print "0" }' coverage/lcov.info) + echo "Mobile coverage: ${COVERAGE}%" + if [ -z "$COVERAGE" ] || [ "$COVERAGE" = "0" ] || [ "$(echo "$COVERAGE < 90" | bc -l)" -eq 1 ]; then + echo "Coverage ${COVERAGE}% is below 90% threshold" + exit 1 + fi + + - name: Test react package with coverage + working-directory: packages/dart_node_react + run: | + dart test --coverage=coverage + dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --report-on=lib + COVERAGE=$(awk -F: '/^LF:/ { total += $2 } /^LH:/ { covered += $2 } END { if (total > 0) printf "%.1f", (covered / total) * 100; else print "0" }' coverage/lcov.info) + echo "React package coverage: ${COVERAGE}%" + if [ -z "$COVERAGE" ] || [ "$COVERAGE" = "0" ] || [ "$(echo "$COVERAGE < 90" | bc -l)" -eq 1 ]; then + echo "Coverage ${COVERAGE}% is below 90% threshold" + exit 1 + fi + + - name: Build frontend + run: dart run tools/build/build.dart frontend + + - name: Build mobile + run: dart run tools/build/build.dart mobile diff --git a/.gitignore b/.gitignore index 7f90cce..1c5517a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ examples/mobile/rn/.expo/ website/_site/ website/src/api/ website/.dart-doc-temp/ + +examples/frontend/coverage/ diff --git a/CLAUDE.md b/CLAUDE.md index 406bcd5..d0c32fb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,38 +4,26 @@ This is a project for Dart packages to be consumed on Node for building node-bas # Rules - All Dart. Absolutely minimal JS +- Use async/await. Do not use `.then` - NO DUPLICATION. Move files, code elements instead of copying them. Search for elements before adding them. HIGHEST PRIORITY. PRIORITIZE THIS OVER ALL ELSE!! -- Return Result from the nadz library for any function that could throw an exception <- CRITICAL!!! +- Prefer typedef records with named fields instead of classes for data (structural typing). This mimics Typescript better +- Return Result from the nadz library for any function that could throw an exception. NO THROWING EXCEPTIONS. +- Avoid casting!!! [! `as` `late`] are all ILLEGAL!!! U +- Use pattern matching switch expressions or ternaries. The exceptional case is if inside arrays and maps because these are declarative and not imperaative. - All packages MUST have austerity installed for linting and nadz for Result types -- Fix ALL lint errors -- Do not expose raw JS objects like JSAny to the higher levels. The library packages are supposed to put a TYPED layer over these -- NO GLOBAL STATE -- Casting, ! etc are all ILLEGAL!!! -- Move non-example-specific code to the framework packages +- Do not expose `JSObject` or `JSAny` etc in the public APIs. Put types over everything. The library packages are supposed to put a TYPED layer over these +- No global state - No skipping tests EVER!!! Agressively unskip tests when you find them!! - Failing tests = OK. Removing assertions or tests = ILLEGAL!! -- NO THROWING EXCEPTIONS. Return results. Handle errors with Result types, except for cases where the code is a placeholder. -- NO PLACEHOLDERS!!! If you HAVE TO leave a section blank, fail LOUDLY by throwing an exception. -- Tests must FAIL HARD. Don't add allowances and print warnings. Just FAIL! +- NO PLACEHOLDERS!!! If you HAVE TO leave a section blank, fail LOUDLY by throwing an exception. This is the only time exceptions are allowed. Tests must FAIL HARD. Don't add allowances and print warnings. Just FAIL! - Keep functions under 20 lines long and files under 500 loc -- NEVER use the late keyword - Do not use Git commands unless explicitly requested -- Don't use if statements. Use pattern matching or ternaries instead. The exceptional case is if inside arrays and maps because these are declarative and not imperaative. ## Build & Run Commands ```bash -# Build express_server example (compiles Dart to Node-compatible JS) -dart run tools/build/build.dart express_server - -# Run the compiled server -node examples/express_server/build/server.js - -# Install Node dependencies for express example -cd examples/express_server && npm install - -# Run tests for express example -cd examples/express_server && dart test +// Build everything +sh run_dev.sh ``` Critical documentation URLs for the Dart JS Framework project. @@ -72,7 +60,7 @@ Critical documentation URLs for the Dart JS Framework project. | Express 4.x API Reference | https://expressjs.com/en/4x/api.html | | Express DevDocs (offline) | https://devdocs.io/express/ | -## React (Phase 2 - Web Frontend) +## React | Topic | URL | |-------|-----| @@ -81,7 +69,7 @@ Critical documentation URLs for the Dart JS Framework project. | Hooks API Reference | https://legacy.reactjs.org/docs/hooks-reference.html | | React DevDocs (offline) | https://devdocs.io/react/ | -## React Native / Expo (Phase 3 - Mobile) +## React Native / Expo | Topic | URL | |-------|-----| @@ -102,10 +90,7 @@ Critical documentation URLs for the Dart JS Framework project. - React 18 docs at `18.react.dev` are canonical - Expo SDK releases 3x/year, targets specific React Native versions -## Architecture - -- ## Testing -Tests in `examples/express_server/test/` use the standard `package:test`. The test spawns the Node server process and makes HTTP requests against it. The server must be built before running tests. +All projects MUST have tests. Where the package is a UI project, the tests MUST test the UI interactions and avoid unit testing. Tests are Dart only. No Javascript unless it's necessary to test the underlying interop. diff --git a/README.md b/README.md index e166e14..136ca91 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,22 @@ graph TD ## Example Quick Start **Web + Backend:** + +Switch to local dependency references. You need to do this before running everything. + ```bash -./run_dev.sh +dart tools/switch_deps.dart local +``` + +Install for all packages and run servers + +```bash +sh run_dev.sh ``` Open http://localhost:8080/web/ +Use `dart tools/switch_deps.dart release` to switch back to release dependencies. + **Mobile:** Use VSCode launch config `Mobile: Build & Run (Expo)` ```mermaid diff --git a/docs/JSX_IMPLEMENTATION_PLAN.md b/docs/JSX_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..39f79d6 --- /dev/null +++ b/docs/JSX_IMPLEMENTATION_PLAN.md @@ -0,0 +1,586 @@ +# JSX Implementation Plan for dart_node_react + +## Overview + +This document outlines the strategy for bringing JSX-like syntax to `dart_node_react`. Since Dart doesn't support JSX natively, we need creative approaches using Dart's language features. + +## Goals + +1. **Reduce Boilerplate** - Make component creation as concise as possible +2. **Maintain Type Safety** - Leverage Dart's type system +3. **Zero Runtime Overhead** - Use extension types and compile-time features +4. **Interop Seamlessly** - Work with existing element factories and React bindings +5. **Idiomatic Dart** - Feel natural to Dart developers + +## Current State Analysis + +### What We Have Now +```dart +// Current approach - verbose but functional +div( + className: 'container', + style: {'padding': 20}, + children: [ + h1('Title'), + p('Some text'), + button(text: 'Click', onClick: () => doSomething()), + ], +) +``` + +### What We Want +```dart +// Ideal: Concise, readable, type-safe +$div(className: 'container', padding: 20) >> [ + $h1 >> 'Title', + $p >> 'Some text', + $button(onClick: doSomething) >> 'Click', +] + +// OR using call syntax +div.c['container']( + h1('Title'), + p('Some text'), + button('Click', onClick: doSomething), +) + +// OR using extension types with builder pattern +Div() + .className('container') + .padding(20) + .children([ + H1().text('Title'), + P().text('Some text'), + Button().text('Click').onClick(doSomething), + ]) + .build() +``` + +## Implementation Approaches + +### Approach 1: Operator Overloading (Recommended First Step) + +Use `>>` operator to chain elements and children: + +```dart +// jsx.dart +extension type El._(T element) implements ReactElement { + El(T element) : this._(element); + + // Child operator - single child + T operator >>(Object child) => _withChild(child); + + // Children operator + T operator >>>(List children) => _withChildren(children); +} + +// Element factories return El instead of T +El $div({String? className, ...}) => El(div(className: className)); +``` + +**Usage:** +```dart +$div(className: 'app') >> [ + $h1 >> 'Welcome', + $div(className: 'content') >> [ + $p >> 'Hello world', + $button(onClick: handleClick) >> 'Submit', + ], +] +``` + +**Pros:** +- Concise syntax +- Easy to implement +- Works today without macros + +**Cons:** +- Non-standard Dart idiom +- Learning curve + +### Approach 2: Extension Types with Fluent API + +Create fluent builders for each element: + +```dart +// jsx_builders.dart +extension type DivBuilder._(Map _props) { + factory DivBuilder() => DivBuilder._({}); + + DivBuilder className(String value) => _set('className', value); + DivBuilder id(String value) => _set('id', value); + DivBuilder style(Map value) => _set('style', value); + + // Direct style props + DivBuilder padding(Object value) => _setStyle('padding', value); + DivBuilder margin(Object value) => _setStyle('margin', value); + DivBuilder display(String value) => _setStyle('display', value); + + // Event handlers + DivBuilder onClick(void Function() handler) => _set('onClick', handler); + + // Terminal: builds the element + DivElement child(ReactElement element) => div(props: _props, child: element); + DivElement children(List elements) => div(props: _props, children: elements); + DivElement text(String content) => div(props: _props, child: span(content)); + DivElement get empty => div(props: _props); +} + +// Shorthand factories +DivBuilder get Div => DivBuilder(); +H1Builder get H1 => H1Builder(); +``` + +**Usage:** +```dart +Div.className('app').padding(20).children([ + H1.text('Welcome'), + Div.className('content').children([ + P.text('Hello world'), + Button.onClick(handleClick).text('Submit'), + ]), +]) +``` + +**Pros:** +- Type-safe props +- IDE autocomplete for all style/event props +- No operator magic + +**Cons:** +- More verbose than JSX +- Lots of boilerplate in implementation + +### Approach 3: Tagged Element Constructors + +Use Dart's record syntax for a DSL: + +```dart +// jsx_records.dart +typedef Props = Map; + +ReactElement jsx(String tag, Props props, [List? children]) { + return domElement(tag, props, _normalizeChildren(children)); +} + +// Shorthand with records +(String, Props?, List?) $div = ('div', null, null); + +extension JsxTuple on (String, Props?, List?) { + ReactElement call([List? children]) => + jsx($1, $2 ?? {}, children ?? $3); +} +``` + +### Approach 4: Code Generation (Future - Requires Build Runner) + +Generate typed element builders from React type definitions: + +```dart +// In build.yaml, use source_gen to create: +// - Typed props classes +// - Builder methods +// - Factory functions + +@GenerateJsxBuilder() +abstract class DivProps { + String? className; + String? id; + void Function()? onClick; + // ... all valid div props +} + +// Generates: +class DivBuilder extends ElementBuilder { + // ... all typed methods +} +``` + +### Approach 5: Dart Macros (Future - Dart 3.5+) + +When Dart macros stabilize: + +```dart +@jsx +class MyComponent { + ReactElement build() { + return jsx''' +
+

Welcome

+

Hello {name}

+
+ '''; + } +} +``` + +## Recommended Implementation Order + +### Phase 1: Operator-Based DSL (Immediate) + +Create `jsx.dart` with: +1. `El` extension type for operator overloading +2. `$div`, `$h1`, `$p`, etc. factory functions +3. Support for text children via `>>` operator +4. Support for list children via `>>` with lists + +**File:** `packages/dart_node_react/lib/src/jsx.dart` + +### Phase 2: Fluent Builders (Short-term) + +Create `jsx_builders.dart` with: +1. Builder extension types for all elements +2. Type-safe style props (padding, margin, display, etc.) +3. Type-safe event handlers +4. Terminal methods (`.child()`, `.children()`, `.text()`) + +**File:** `packages/dart_node_react/lib/src/jsx_builders.dart` + +### Phase 3: Convenience Extensions (Medium-term) + +Add quality-of-life improvements: +1. String extension for text nodes: `'Hello'.el` +2. Int extension for spacers: `20.px` +3. Conditional rendering helpers +4. List flattening for fragments + +**File:** `packages/dart_node_react/lib/src/jsx_extensions.dart` + +### Phase 4: Code Generation (Long-term) + +If adoption warrants it: +1. Create `jsx_generator` package +2. Generate builders from React type defs +3. Auto-generate prop types + +### Phase 5: Macros (When Available) + +When Dart macros are stable: +1. True JSX-like string interpolation +2. Compile-time transformation + +## Implementation Details + +### Phase 1: `jsx.dart` + +```dart +// packages/dart_node_react/lib/src/jsx.dart + +import 'dart:js_interop'; +import 'elements.dart'; +import 'html_elements.dart'; +import 'react.dart'; + +/// Extension type that adds operator overloading for element composition +extension type El._(T _element) implements ReactElement { + /// Creates an El wrapper around a ReactElement + El(T element) : this._(element); + + /// Adds a single child or list of children using >> operator + /// + /// Usage: + /// ```dart + /// $div >> 'text' + /// $div >> [$h1 >> 'Title', $p >> 'Content'] + /// ``` + T operator >>(Object child) => switch (child) { + String text => _withTextChild(text), + List children => _withChildren(children), + ReactElement element => _withSingleChild(element), + El el => _withSingleChild(el._element), + _ => throw ArgumentError('Invalid child type: ${child.runtimeType}'), + }; + + T _withTextChild(String text) { + // Get the element type and recreate with text child + final props = _element.props; + final type = _element.type; + return createElement( + type, + props, + text.toJS, + ) as T; + } + + T _withSingleChild(ReactElement child) { + final props = _element.props; + final type = _element.type; + return createElement(type, props, child) as T; + } + + T _withChildren(List children) { + final normalizedChildren = children.map(_normalizeChild).toList(); + final props = _element.props; + final type = _element.type; + return createElementWithChildren( + type, + props, + normalizedChildren.cast(), + ) as T; + } + + JSAny _normalizeChild(Object child) => switch (child) { + String text => text.toJS, + ReactElement element => element, + El el => el._element, + int n => n.toString().toJS, + double n => n.toString().toJS, + _ => throw ArgumentError('Invalid child: ${child.runtimeType}'), + }; +} + +// Element factory functions that return El +El $div({ + String? className, + String? id, + Map? style, + Map? props, + void Function()? onClick, +}) => El(div( + className: className, + style: style, + props: _mergeProps(props, id: id, onClick: onClick), +)); + +El $h1({ + String? className, + String? id, + Map? style, + Map? props, +}) => El(h1('', className: className, style: style, props: _mergeProps(props, id: id))); + +El $h2({ + String? className, + String? id, + Map? style, + Map? props, +}) => El(h2('', className: className, style: style, props: _mergeProps(props, id: id))); + +El $p({ + String? className, + String? id, + Map? style, + Map? props, +}) => El(pEl('', className: className, style: style, props: _mergeProps(props, id: id))); + +El $span({ + String? className, + String? id, + Map? style, + Map? props, +}) => El(span('', className: className, style: style, props: _mergeProps(props, id: id))); + +El $button({ + String? className, + String? id, + Map? style, + Map? props, + void Function()? onClick, +}) => El(button(text: '', className: className, style: style, props: props, onClick: onClick)); + +// ... more element factories + +Map? _mergeProps( + Map? props, { + String? id, + void Function()? onClick, +}) { + final hasExtra = id != null || onClick != null; + if (props == null && !hasExtra) return null; + return { + ...?props, + if (id != null) 'id': id, + if (onClick != null) 'onClick': onClick, + }; +} +``` + +### Children Normalization + +Handle various child types: + +```dart +// Text: 'Hello' → JSString +// Element: div(...) → as-is +// Number: 42 → '42'.toJS +// List: [...] → flattened JSArray +// Null: null → filtered out +// Conditional: condition ? element : null → filtered out +``` + +### Style Shorthand Props + +For the fluent builders (Phase 2): + +```dart +extension type DivBuilder._(Map _props) { + // Layout + DivBuilder display(String v) => _style('display', v); + DivBuilder flex(int v) => _style('flex', v); + DivBuilder flexDirection(String v) => _style('flexDirection', v); + DivBuilder alignItems(String v) => _style('alignItems', v); + DivBuilder justifyContent(String v) => _style('justifyContent', v); + + // Spacing + DivBuilder padding(Object v) => _style('padding', v); + DivBuilder paddingX(Object v) => this.paddingLeft(v).paddingRight(v); + DivBuilder paddingY(Object v) => this.paddingTop(v).paddingBottom(v); + DivBuilder margin(Object v) => _style('margin', v); + + // Size + DivBuilder width(Object v) => _style('width', v); + DivBuilder height(Object v) => _style('height', v); + DivBuilder minWidth(Object v) => _style('minWidth', v); + DivBuilder maxWidth(Object v) => _style('maxWidth', v); + + // Colors + DivBuilder backgroundColor(String v) => _style('backgroundColor', v); + DivBuilder color(String v) => _style('color', v); + + // Border + DivBuilder border(String v) => _style('border', v); + DivBuilder borderRadius(Object v) => _style('borderRadius', v); + + // ... etc +} +``` + +## File Structure + +``` +packages/dart_node_react/lib/src/ +├── jsx.dart # Phase 1: Operator-based DSL +├── jsx_builders.dart # Phase 2: Fluent builders +├── jsx_extensions.dart # Phase 3: Convenience extensions +└── (existing files) # Untouched existing implementation +``` + +## Export Strategy + +Add to `dart_node_react.dart`: + +```dart +// Core API (existing) +export 'src/react.dart'; +export 'src/elements.dart'; +// ... other existing exports + +// JSX-like API (new) +export 'src/jsx.dart'; +export 'src/jsx_builders.dart'; // Phase 2 +export 'src/jsx_extensions.dart'; // Phase 3 +``` + +## Example: Counter Component + +### Current API +```dart +ReactElement Counter() { + final count = useState(0); + + return div( + className: 'counter', + style: {'padding': 20, 'textAlign': 'center'}, + children: [ + h1('Counter: ${count.value}'), + div( + style: {'display': 'flex', 'gap': 10, 'justifyContent': 'center'}, + children: [ + button(text: '-', onClick: () => count.set(count.value - 1)), + button(text: '+', onClick: () => count.set(count.value + 1)), + ], + ), + ], + ); +} +``` + +### Phase 1: Operator DSL +```dart +ReactElement Counter() { + final count = useState(0); + + return $div(className: 'counter', style: {'padding': 20, 'textAlign': 'center'}) >> [ + $h1() >> 'Counter: ${count.value}', + $div(style: {'display': 'flex', 'gap': 10, 'justifyContent': 'center'}) >> [ + $button(onClick: () => count.set(count.value - 1)) >> '-', + $button(onClick: () => count.set(count.value + 1)) >> '+', + ], + ]; +} +``` + +### Phase 2: Fluent Builders +```dart +ReactElement Counter() { + final count = useState(0); + + return Div + .className('counter') + .padding(20) + .textAlign('center') + .children([ + H1.text('Counter: ${count.value}'), + Div.display('flex').gap(10).justifyContent('center').children([ + Button.onClick(() => count.set(count.value - 1)).text('-'), + Button.onClick(() => count.set(count.value + 1)).text('+'), + ]), + ]); +} +``` + +## Testing Strategy + +Create `test/jsx_test.dart`: + +```dart +void main() { + group('JSX Operator DSL', () { + test('creates element with text child', () { + final el = $h1() >> 'Hello'; + expect(el, isA()); + }); + + test('creates element with element child', () { + final el = $div() >> ($span() >> 'text'); + expect(el, isA()); + }); + + test('creates element with multiple children', () { + final el = $div() >> [ + $h1() >> 'Title', + $p() >> 'Content', + ]; + expect(el, isA()); + }); + + test('preserves props', () { + final el = $div(className: 'test', id: 'my-id') >> 'content'; + // verify props are set correctly + }); + }); +} +``` + +## Compatibility Notes + +1. **Existing API Unchanged** - All current `div()`, `h1()`, etc. functions remain +2. **Opt-in** - Import `jsx.dart` to use new syntax +3. **Interoperable** - Can mix old and new syntax freely +4. **No Breaking Changes** - Pure additive feature + +## Open Questions + +1. **Operator Choice** - Is `>>` the best operator? Alternatives: `|`, `%`, `&` +2. **Naming** - `$div` vs `Div` vs `div_` for factories +3. **Null Children** - How to handle conditional `null` children in lists? +4. **Keys** - How to specify React keys in the new syntax? +5. **Ref Forwarding** - How to attach refs cleanly? + +## Next Steps + +1. Create `jsx.dart` with Phase 1 implementation +2. Add tests for operator behavior +3. Document usage patterns +4. Gather feedback on syntax preferences +5. Iterate on API design diff --git a/examples/backend/lib/schemas.dart b/examples/backend/lib/schemas.dart index 593fec6..7ca8de1 100644 --- a/examples/backend/lib/schemas.dart +++ b/examples/backend/lib/schemas.dart @@ -44,12 +44,6 @@ final createUserSchema = schema( /// Validation schema for login final loginSchema = schema( - { - 'email': string().email(), - 'password': string().notEmpty(), - }, - (map) => ( - email: map['email'] as String, - password: map['password'] as String, - ), + {'email': string().email(), 'password': string().notEmpty()}, + (map) => (email: map['email'] as String, password: map['password'] as String), ); diff --git a/examples/backend/lib/services/token_service.dart b/examples/backend/lib/services/token_service.dart index b933355..80a3ca9 100644 --- a/examples/backend/lib/services/token_service.dart +++ b/examples/backend/lib/services/token_service.dart @@ -15,8 +15,9 @@ class TokenService { final payload = { 'userId': userId, 'iat': DateTime.now().millisecondsSinceEpoch, - 'exp': - DateTime.now().add(const Duration(hours: 24)).millisecondsSinceEpoch, + 'exp': DateTime.now() + .add(const Duration(hours: 24)) + .millisecondsSinceEpoch, }; // Simple base64 encoding (NOT secure - just for demo) diff --git a/examples/backend/server.dart b/examples/backend/server.dart index be63eb6..d4e1089 100644 --- a/examples/backend/server.dart +++ b/examples/backend/server.dart @@ -182,18 +182,18 @@ JSFunction jsonParser() { /// CORS middleware JSFunction cors() => ((Request req, Response res, JSNextFunction next) { - res - ..set('Access-Control-Allow-Origin', '*') - ..set('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS') - ..set('Access-Control-Allow-Headers', 'Content-Type,Authorization'); - if (req.method == 'OPTIONS') { - res - ..status(204) - ..end(); - return; - } - next(); - }).toJS; + res + ..set('Access-Control-Allow-Origin', '*') + ..set('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS') + ..set('Access-Control-Allow-Headers', 'Content-Type,Authorization'); + if (req.method == 'OPTIONS') { + res + ..status(204) + ..end(); + return; + } + next(); +}).toJS; /// Authentication context typedef AuthContext = ({User user, String token}); @@ -202,10 +202,7 @@ typedef AuthContext = ({User user, String token}); final _authContexts = {}; /// Auth middleware -JSFunction authenticate( - TokenService tokenService, - UserService userService, -) => +JSFunction authenticate(TokenService tokenService, UserService userService) => ((Request req, Response res, JSNextFunction next) { final authHeader = req.headers['authorization']; switch (authHeader) { @@ -237,8 +234,10 @@ JSFunction authenticate( ..jsonMap({'error': 'User not found'}); return; case final u: - _authContexts[(req as JSObject).hashCode] = - (user: u, token: token); + _authContexts[(req as JSObject).hashCode] = ( + user: u, + token: token, + ); next(); } } diff --git a/examples/backend/test/server_test.dart b/examples/backend/test/server_test.dart index fcd90b3..8963c88 100644 --- a/examples/backend/test/server_test.dart +++ b/examples/backend/test/server_test.dart @@ -15,11 +15,9 @@ void main() { final currentDir = Directory.current.path; // Start the server - serverProcess = await Process.start( - 'node', - ['build/server.js'], - workingDirectory: currentDir, - ); + serverProcess = await Process.start('node', [ + 'build/server.js', + ], workingDirectory: currentDir); // Wait for server to be ready await Future.delayed(const Duration(seconds: 2)); @@ -275,10 +273,7 @@ void main() { 'Content-Type': 'application/json', 'Authorization': 'Bearer $authToken', }, - body: jsonEncode({ - 'title': 'Updated Title', - 'completed': true, - }), + body: jsonEncode({'title': 'Updated Title', 'completed': true}), ); expect(response.statusCode, equals(200)); diff --git a/examples/frontend/dart_test.yaml b/examples/frontend/dart_test.yaml new file mode 100644 index 0000000..1d9e304 --- /dev/null +++ b/examples/frontend/dart_test.yaml @@ -0,0 +1,8 @@ +platforms: [chrome] + +override_platforms: + chrome: + settings: + arguments: --disable-gpu --no-sandbox + +custom_html_template_path: test/test_template.html diff --git a/examples/frontend/lib/frontend.dart b/examples/frontend/lib/frontend.dart new file mode 100644 index 0000000..7115a9f --- /dev/null +++ b/examples/frontend/lib/frontend.dart @@ -0,0 +1,10 @@ +/// Frontend components library +library; + +export 'src/form_helpers.dart'; +export 'src/header.dart'; +export 'src/login_form.dart'; +export 'src/register_form.dart'; +export 'src/task_components.dart'; +export 'src/types.dart'; +export 'src/websocket.dart'; diff --git a/examples/frontend/lib/src/form_helpers.dart b/examples/frontend/lib/src/form_helpers.dart new file mode 100644 index 0000000..30374a6 --- /dev/null +++ b/examples/frontend/lib/src/form_helpers.dart @@ -0,0 +1,25 @@ +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +import 'package:dart_node_react/dart_node_react.dart'; + +/// Create a form group with label +DivElement formGroup(String labelText, ReactElement inputElement) => + div(className: 'form-group', children: [labelEl(labelText), inputElement]); + +/// Create a label element +ReactElement labelEl(String text) => + createElement('label'.toJS, createProps({'className': 'label'}), text.toJS); + +/// Extract input value from event +JSString getInputValue(JSAny event) { + final obj = event as JSObject; + final target = obj['target']; + return switch (target) { + final JSObject t => switch (t['value']) { + final JSString v => v, + _ => throw StateError('Input value is not a string'), + }, + _ => throw StateError('Event target is not an object'), + }; +} diff --git a/examples/frontend/lib/src/header.dart b/examples/frontend/lib/src/header.dart new file mode 100644 index 0000000..a56ee83 --- /dev/null +++ b/examples/frontend/lib/src/header.dart @@ -0,0 +1,33 @@ +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +import 'package:dart_node_core/dart_node_core.dart'; +import 'package:dart_node_react/dart_node_react.dart'; +import 'package:frontend/src/types.dart'; + +/// Build app header component +HeaderElement buildHeader(JSObject? user, OnClick onLogout) => header( + className: 'header', + children: [ + div( + className: 'header-content', + children: [ + h1('TaskFlow', className: 'logo'), + (user?['name']?.toString()).match( + some: (userName) => div( + className: 'user-info', + children: [ + span('Welcome, $userName', className: 'user-name'), + button( + text: 'Logout', + className: 'btn btn-ghost', + onClick: onLogout, + ), + ], + ), + none: () => span('', className: 'spacer'), + ), + ], + ), + ], +); diff --git a/examples/frontend/lib/src/login_form.dart b/examples/frontend/lib/src/login_form.dart new file mode 100644 index 0000000..061e72e --- /dev/null +++ b/examples/frontend/lib/src/login_form.dart @@ -0,0 +1,110 @@ +import 'dart:async'; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +import 'package:dart_node_react/dart_node_react.dart'; +import 'package:frontend/src/form_helpers.dart'; +import 'package:frontend/src/types.dart'; +import 'package:nadz/nadz.dart'; +import 'package:shared/http/http_client.dart'; + +/// Build login form component +ReactElement buildLoginForm( + AuthEffects auth, { + String baseUrl = apiUrl, + Fetch? fetchFn, +}) { + final doFetch = fetchFn ?? fetch; + return createElement( + ((JSAny props) { + final emailState = useState(''); + final passState = useState(''); + final errorState = useState(null); + final loadingState = useState(false); + + void handleSubmit() { + loadingState.set(true); + errorState.set(null); + + unawaited( + doFetch( + '$baseUrl/auth/login', + method: 'POST', + body: {'email': emailState.value, 'password': passState.value}, + ) + .then((result) { + result.match( + onSuccess: (response) { + final data = response['data']; + switch (data) { + case null: + errorState.set('Login failed'); + case final JSObject details: + switch (details['token']) { + case final JSString token: + auth.setToken(token); + auth.setUser(details['user'] as JSObject?); + default: + errorState.set('No token'); + } + } + }, + onError: errorState.set, + ); + }) + .catchError((Object e) { + errorState.set(e.toString()); + }) + .whenComplete(() => loadingState.set(false)), + ); + } + + return div( + className: 'auth-card', + children: [ + h2('Sign In', className: 'auth-title'), + if (errorState.value != null) + div(className: 'error-msg', child: span(errorState.value!)) + else + span(''), + formGroup( + 'Email', + input( + type: 'email', + placeholder: 'you@example.com', + value: emailState.value, + className: 'input', + onChange: (e) => emailState.set(getInputValue(e).toDart), + ), + ), + formGroup( + 'Password', + input( + type: 'password', + placeholder: '••••••••', + value: passState.value, + className: 'input', + onChange: (e) => passState.set(getInputValue(e).toDart), + ), + ), + button( + text: loadingState.value ? 'Signing in...' : 'Sign In', + className: 'btn btn-primary btn-full', + onClick: loadingState.value ? null : handleSubmit, + ), + div( + className: 'auth-footer', + children: [ + span("Don't have an account? "), + button( + text: 'Register', + className: 'btn-link', + onClick: () => auth.setView('register'), + ), + ], + ), + ], + ); + }).toJS, + ); +} diff --git a/examples/frontend/lib/src/register_form.dart b/examples/frontend/lib/src/register_form.dart new file mode 100644 index 0000000..bb55e57 --- /dev/null +++ b/examples/frontend/lib/src/register_form.dart @@ -0,0 +1,125 @@ +import 'dart:async'; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +import 'package:dart_node_react/dart_node_react.dart'; +import 'package:frontend/src/form_helpers.dart'; +import 'package:frontend/src/types.dart'; +import 'package:nadz/nadz.dart'; +import 'package:shared/http/http_client.dart'; + +/// Build register form component +ReactElement buildRegisterForm( + AuthEffects auth, { + String baseUrl = apiUrl, + Fetch? fetchFn, +}) { + final doFetch = fetchFn ?? fetch; + return createElement( + ((JSAny props) { + final nameState = useState(''); + final emailState = useState(''); + final passState = useState(''); + final errorState = useState(null); + final loadingState = useState(false); + + void handleSubmit() { + loadingState.set(true); + errorState.set(null); + + unawaited( + doFetch( + '$baseUrl/auth/register', + method: 'POST', + body: { + 'email': emailState.value, + 'password': passState.value, + 'name': nameState.value, + }, + ) + .then((result) { + result.match( + onSuccess: (response) { + final data = response['data']; + switch (data) { + case null: + errorState.set('Registration failed'); + case final JSObject details: + switch (details['token']) { + case final JSString token: + auth.setToken(token); + auth.setUser(details['user'] as JSObject?); + default: + errorState.set('No token'); + } + } + }, + onError: errorState.set, + ); + }) + .catchError((Object e) { + errorState.set(e.toString()); + }) + .whenComplete(() => loadingState.set(false)), + ); + } + + return div( + className: 'auth-card', + children: [ + h2('Create Account', className: 'auth-title'), + if (errorState.value != null) + div(className: 'error-msg', child: span(errorState.value!)) + else + span(''), + formGroup( + 'Name', + input( + type: 'text', + placeholder: 'Your name', + value: nameState.value, + className: 'input', + onChange: (e) => nameState.set(getInputValue(e).toDart), + ), + ), + formGroup( + 'Email', + input( + type: 'email', + placeholder: 'you@example.com', + value: emailState.value, + className: 'input', + onChange: (e) => emailState.set(getInputValue(e).toDart), + ), + ), + formGroup( + 'Password', + input( + type: 'password', + placeholder: '••••••••', + value: passState.value, + className: 'input', + onChange: (e) => passState.set(getInputValue(e).toDart), + ), + ), + button( + text: loadingState.value ? 'Creating...' : 'Create Account', + className: 'btn btn-primary btn-full', + onClick: loadingState.value ? null : handleSubmit, + ), + div( + className: 'auth-footer', + children: [ + span('Already have an account? '), + button( + text: 'Sign In', + className: 'btn-link', + onClick: () => auth.setView('login'), + ), + ], + ), + ], + ); + }).toJS, + ); +} diff --git a/examples/frontend/lib/src/task_components.dart b/examples/frontend/lib/src/task_components.dart new file mode 100644 index 0000000..7b4a499 --- /dev/null +++ b/examples/frontend/lib/src/task_components.dart @@ -0,0 +1,86 @@ +import 'dart:js_interop'; + +import 'package:dart_node_react/dart_node_react.dart'; +import 'package:shared/js_types/js_types.dart'; + +/// Build stats display for task list +DivElement buildStats(List tasks) { + final total = tasks.length; + final completed = tasks.where((t) => t.completed).length; + final pct = total > 0 ? (completed / total * 100).round() : 0; + return div( + className: 'stats', + children: [ + span('$completed/$total completed', className: 'stat-text'), + div( + className: 'progress-bar', + child: div( + className: 'progress-fill', + props: { + 'style': {'width': '$pct%'}.jsify(), + }, + ), + ), + ], + ); +} + +/// Build task list or empty state +List buildTaskList( + List tasks, + void Function(String, bool) onToggle, + void Function(String) onDelete, +) => tasks.isEmpty + ? [ + div( + className: 'empty-state', + children: [ + pEl('No tasks yet. Add one above!', className: 'empty-text'), + ], + ), + ] + : tasks.map((task) => buildTaskItem(task, onToggle, onDelete)).toList(); + +/// Build a single task item +DivElement buildTaskItem( + JSTask task, + void Function(String, bool) onToggle, + void Function(String) onDelete, +) { + final checkClass = task.completed + ? 'task-checkbox completed' + : 'task-checkbox'; + final titleClass = task.completed ? 'task-title completed' : 'task-title'; + final itemClass = task.completed ? 'task-item completed' : 'task-item'; + final description = task.description; + + return div( + className: itemClass, + children: [ + div( + className: checkClass, + props: { + 'onClick': ((JSAny? _) => onToggle(task.id, task.completed)).toJS, + }, + child: task.completed + ? span('\u2713', className: 'check-icon') + : span(''), + ), + div( + className: 'task-content', + children: [ + span(task.title, className: titleClass), + if (description != null && description.isNotEmpty) + span(description, className: 'task-desc') + else + span(''), + ], + ), + button( + text: '\u00D7', + className: 'btn-delete', + onClick: () => onDelete(task.id), + ), + ], + ); +} diff --git a/examples/frontend/lib/src/types.dart b/examples/frontend/lib/src/types.dart new file mode 100644 index 0000000..eb51033 --- /dev/null +++ b/examples/frontend/lib/src/types.dart @@ -0,0 +1,33 @@ +import 'dart:js_interop'; + +import 'package:shared/http/http_client.dart'; + +// Re-export shared JS types +export 'package:shared/js_types/js_types.dart'; + +/// API configuration +const apiUrl = 'http://localhost:3000'; + +/// Replaceable fetch - swap this for testing +Fetch fetch = fetchJson; + +/// WebSocket URL +const wsUrl = 'ws://localhost:3001'; + +/// Auth actions - typed setters for authentication state +typedef SetToken = void Function(JSString?); +typedef SetUser = void Function(JSObject?); +typedef SetView = void Function(String); + +/// Auth effects bundle - passed to form components +typedef AuthEffects = ({SetToken setToken, SetUser setUser, SetView setView}); + +/// Task operations +typedef OnToggleTask = void Function(String id, bool completed); +typedef OnDeleteTask = void Function(String id); + +/// Task effects bundle +typedef TaskEffects = ({OnToggleTask onToggle, OnDeleteTask onDelete}); + +/// Event handler effect +typedef OnClick = void Function(); diff --git a/examples/frontend/web/websocket.dart b/examples/frontend/lib/src/websocket.dart similarity index 85% rename from examples/frontend/web/websocket.dart rename to examples/frontend/lib/src/websocket.dart index 21d7e00..12615f7 100644 --- a/examples/frontend/web/websocket.dart +++ b/examples/frontend/lib/src/websocket.dart @@ -1,7 +1,7 @@ import 'dart:js_interop'; import 'dart:js_interop_unsafe'; -import 'types.dart'; +import 'package:frontend/src/types.dart'; /// Browser WebSocket extension type extension type BrowserWebSocket(JSObject _) implements JSObject { @@ -28,7 +28,7 @@ extension type MessageEvent(JSObject _) implements JSObject { } /// Create a new WebSocket connection -BrowserWebSocket _createWebSocket(String url) { +BrowserWebSocket createWebSocket(String url) { final wsCtor = globalContext['WebSocket']! as JSFunction; return BrowserWebSocket(wsCtor.callAsConstructor(url.toJS)); } @@ -39,8 +39,9 @@ BrowserWebSocket? connectWebSocket({ required void Function(JSObject event) onTaskEvent, void Function()? onOpen, void Function()? onClose, + String baseWsUrl = wsUrl, }) { - final ws = _createWebSocket('$wsUrl?token=$token') + final ws = createWebSocket('$baseWsUrl?token=$token') ..onopen = ((JSAny _) { onOpen?.call(); }).toJS @@ -51,7 +52,7 @@ BrowserWebSocket? connectWebSocket({ final message = data.dartify() as String?; switch (message) { case final String m: - _handleMessage(m, onTaskEvent); + handleWebSocketMessage(m, onTaskEvent); case null: break; } @@ -69,7 +70,11 @@ BrowserWebSocket? connectWebSocket({ return ws; } -void _handleMessage(String message, void Function(JSObject) onTaskEvent) { +/// Parse and handle incoming WebSocket message +void handleWebSocketMessage( + String message, + void Function(JSObject) onTaskEvent, +) { final json = globalContext['JSON']! as JSObject; final parseFn = json['parse']! as JSFunction; final parsed = parseFn.callAsFunction(null, message.toJS)! as JSObject; diff --git a/examples/frontend/pubspec.lock b/examples/frontend/pubspec.lock index c3b0aef..cd3b85b 100644 --- a/examples/frontend/pubspec.lock +++ b/examples/frontend/pubspec.lock @@ -1,6 +1,38 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "5b7468c326d2f8a4f630056404ca0d291ade42918f4a3c6233618e724f39da8e" + url: "https://pub.dev" + source: hosted + version: "92.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "70e4b1ef8003c64793a9e268a551a82869a8a96f39deb73dea28084b0e8bf75e" + url: "https://pub.dev" + source: hosted + version: "9.0.0" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" austerity: dependency: "direct main" description: @@ -9,6 +41,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" dart_node_core: dependency: transitive description: @@ -23,6 +103,54 @@ packages: relative: true source: path version: "0.2.0-beta" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" lints: dependency: "direct dev" description: @@ -31,6 +159,38 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.dev" + source: hosted + version: "0.12.18" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" nadz: dependency: "direct main" description: @@ -47,6 +207,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" shared: dependency: "direct main" description: @@ -54,5 +246,181 @@ packages: relative: true source: path version: "1.0.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: "77cc98ea27006c84e71a7356cf3daf9ddbde2d91d84f77dbfe64cf0e4d9611ae" + url: "https://pub.dev" + source: hosted + version: "1.28.0" + test_api: + dependency: transitive + description: + name: test_api + sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" + url: "https://pub.dev" + source: hosted + version: "0.7.8" + test_core: + dependency: transitive + description: + name: test_core + sha256: f1072617a6657e5fc09662e721307f7fb009b4ed89b19f47175d11d5254a62d4 + url: "https://pub.dev" + source: hosted + version: "0.6.14" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" + url: "https://pub.dev" + source: hosted + version: "1.1.4" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: dart: ">=3.10.0 <4.0.0" diff --git a/examples/frontend/pubspec.yaml b/examples/frontend/pubspec.yaml index 4f6a992..9ed1b36 100644 --- a/examples/frontend/pubspec.yaml +++ b/examples/frontend/pubspec.yaml @@ -16,3 +16,4 @@ dependencies: dev_dependencies: lints: ^5.0.0 + test: ^1.25.0 diff --git a/examples/frontend/test/test_helpers.dart b/examples/frontend/test/test_helpers.dart new file mode 100644 index 0000000..a379337 --- /dev/null +++ b/examples/frontend/test/test_helpers.dart @@ -0,0 +1,148 @@ +/// Test helpers and mock utilities for frontend tests. +library; + +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +import 'package:dart_node_react/src/testing_library.dart'; +import 'package:frontend/src/types.dart'; +import 'package:nadz/nadz.dart'; +import 'package:shared/http/http_client.dart'; + +/// Create a mock AuthEffects +AuthEffects createMockAuth() => ( + setToken: (JSString? _) {}, + setUser: (JSObject? _) {}, + setView: (String _) {}, +); + +/// Create a JSObject from a Dart map +JSObject createJSObject(Map map) { + final json = globalContext['JSON']! as JSObject; + final parseFn = json['parse']! as JSFunction; + final jsonStr = _toJsonString(map); + return parseFn.callAsFunction(null, jsonStr.toJS)! as JSObject; +} + +/// Create a JSTask from a Dart map +JSTask createMockTask(Map map) => + JSTask.fromJS(createJSObject(map)); + +String _toJsonString(Map map) { + final entries = map.entries.map((e) { + final value = e.value; + String valueStr; + if (value is String) { + valueStr = '"$value"'; + } else if (value is bool) { + valueStr = value.toString(); + } else if (value is int) { + valueStr = value.toString(); + } else if (value is double) { + valueStr = value.toString(); + } else if (value == null) { + valueStr = 'null'; + } else if (value is Map) { + valueStr = _toJsonString(value.cast()); + } else if (value is List) { + valueStr = _toJsonList(value.cast()); + } else { + valueStr = '"$value"'; + } + return '"${e.key}":$valueStr'; + }); + return '{${entries.join(',')}}'; +} + +String _toJsonList(List list) { + final items = list.map((item) { + if (item is String) return '"$item"'; + if (item is bool) return item.toString(); + if (item is int) return item.toString(); + if (item is double) return item.toString(); + if (item == null) return 'null'; + if (item is Map) return _toJsonString(item.cast()); + if (item is List) return _toJsonList(item.cast()); + return '"$item"'; + }); + return '[${items.join(',')}]'; +} + +// --- Fetch Mock Infrastructure --- + +/// Create a mock fetch function from URL pattern -> response map +/// +/// Keys can be: +/// - URL pattern: '/tasks' matches any method +/// - Method + URL: 'POST /tasks' matches only POST to /tasks +Fetch createMockFetch(Map> responses) => + (url, {method = 'GET', token, body}) async { + // First try method-specific match (e.g., 'POST /tasks') + final methodKey = '$method $url'.replaceAll(RegExp('https?://[^/]+'), ''); + for (final entry in responses.entries) { + if (entry.key.contains(' ') && methodKey.contains(entry.key)) { + return _buildResponse(entry.value); + } + } + // Fall back to URL-only match + for (final entry in responses.entries) { + // Skip method-prefixed keys for URL-only matching + if (entry.key.contains(' ')) continue; + if (url.contains(entry.key)) { + return _buildResponse(entry.value); + } + } + throw StateError('No mock for $method $url'); + }; + +Result _buildResponse(Map response) { + final success = response['success'] == true; + return success + ? Success(createJSObject(response)) + : Error(response['error']?.toString() ?? 'Request failed'); +} + +/// Create a fetch function that throws to test catchError branches +Fetch createThrowingFetch() => + (url, {method = 'GET', token, body}) => + Future.error(Exception('Network error')); + +// --- WebSocket Mock --- + +JSObject? _lastMockWs; + +void mockWebSocket() { + globalContext['WebSocket'] = ((JSString url) { + final ws = JSObject(); + ws['close'] = (() {}).toJS; + ws['send'] = ((JSAny _) {}).toJS; + _lastMockWs = ws; + return ws; + }).toJS; +} + +/// Simulate WebSocket message from server +void simulateWsMessage(String json) { + final ws = _lastMockWs; + if (ws == null) return; + final onmessage = ws['onmessage']; + if (onmessage == null) return; + final event = JSObject(); + event['data'] = json.toJS; + (onmessage as JSFunction).callAsFunction(null, event); +} + +// --- Wait Helpers --- + +Future waitForText( + TestRenderResult result, + String text, { + int maxAttempts = 20, + Duration interval = const Duration(milliseconds: 100), +}) async { + for (var i = 0; i < maxAttempts; i++) { + if (result.container.textContent.contains(text)) return; + await Future.delayed(interval); + } + throw StateError('Text "$text" not found after $maxAttempts attempts'); +} diff --git a/examples/frontend/test/test_template.html b/examples/frontend/test/test_template.html new file mode 100644 index 0000000..100bb6d --- /dev/null +++ b/examples/frontend/test/test_template.html @@ -0,0 +1,41 @@ + + + + + {{testName}} + + + + + + {{testScript}} + + + diff --git a/examples/frontend/test/web_app_basic_test.dart b/examples/frontend/test/web_app_basic_test.dart new file mode 100644 index 0000000..09b1278 --- /dev/null +++ b/examples/frontend/test/web_app_basic_test.dart @@ -0,0 +1,570 @@ +/// UI interaction tests for the TaskFlow web app. +/// +/// Tests verify actual user interactions using the real lib/ components. +/// Run with: dart test -p chrome +@TestOn('browser') +library; + +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +import 'package:dart_node_react/dart_node_react.dart' hide render; +import 'package:dart_node_react/src/testing_library.dart'; +import 'package:frontend/frontend.dart'; +import 'package:test/test.dart'; + +import 'test_helpers.dart'; + +void main() { + group('Header Component', () { + test('displays user name when logged in', () { + final user = createJSObject({'name': 'John Doe'}); + + final result = render(buildHeader(user, () {})); + + expect(result.container.textContent, contains('Welcome, John Doe')); + expect(result.container.textContent, contains('Logout')); + + result.unmount(); + }); + + test('shows spacer when no user', () { + final result = render(buildHeader(null, () {})); + + expect(result.container.textContent, isNot(contains('Welcome'))); + + result.unmount(); + }); + + test('logout button calls callback when clicked', () { + final user = createJSObject({'name': 'Test User'}); + var logoutCalled = false; + + final result = render(buildHeader(user, () => logoutCalled = true)); + + final logoutBtn = result.container.querySelector('.btn-ghost'); + expect(logoutBtn, isNotNull); + fireClick(logoutBtn!); + + expect(logoutCalled, isTrue); + + result.unmount(); + }); + }); + + group('Login Form Component', () { + test('renders login form with sign in title', () { + final auth = createMockAuth(); + + final result = render(buildLoginForm(auth)); + + expect(result.container.textContent, contains('Sign In')); + + result.unmount(); + }); + + test('renders email and password labels', () { + final auth = createMockAuth(); + + final result = render(buildLoginForm(auth)); + + expect(result.container.textContent, contains('Email')); + expect(result.container.textContent, contains('Password')); + + result.unmount(); + }); + + test('renders register link', () { + final auth = createMockAuth(); + + final result = render(buildLoginForm(auth)); + + expect(result.container.textContent, contains("Don't have an account?")); + expect(result.container.textContent, contains('Register')); + + result.unmount(); + }); + + test('clicking register switches view', () { + var viewValue = ''; + final auth = ( + setToken: (JSString? _) {}, + setUser: (JSObject? _) {}, + setView: (String v) => viewValue = v, + ); + + final result = render(buildLoginForm(auth)); + + final registerBtn = result.container.querySelector('.btn-link'); + expect(registerBtn, isNotNull); + fireClick(registerBtn!); + + expect(viewValue, equals('register')); + + result.unmount(); + }); + }); + + group('Register Form Component', () { + test('renders register form with create account title', () { + final auth = createMockAuth(); + + final result = render(buildRegisterForm(auth)); + + expect(result.container.textContent, contains('Create Account')); + + result.unmount(); + }); + + test('renders name, email, and password labels', () { + final auth = createMockAuth(); + + final result = render(buildRegisterForm(auth)); + + expect(result.container.textContent, contains('Name')); + expect(result.container.textContent, contains('Email')); + expect(result.container.textContent, contains('Password')); + + result.unmount(); + }); + + test('renders sign in link', () { + final auth = createMockAuth(); + + final result = render(buildRegisterForm(auth)); + + expect( + result.container.textContent, + contains('Already have an account?'), + ); + + result.unmount(); + }); + + test('clicking sign in switches view', () { + var viewValue = ''; + final auth = ( + setToken: (JSString? _) {}, + setUser: (JSObject? _) {}, + setView: (String v) => viewValue = v, + ); + + final result = render(buildRegisterForm(auth)); + + final signInBtn = result.container.querySelector('.btn-link'); + expect(signInBtn, isNotNull); + fireClick(signInBtn!); + + expect(viewValue, equals('login')); + + result.unmount(); + }); + }); + + group('Form Helpers', () { + test('formGroup creates labeled input', () { + final inputEl = input( + type: 'text', + className: 'test-input', + props: {'data-testid': 'test-input'}, + ); + final group = formGroup('Test Label', inputEl); + + final result = render(group); + + expect(result.container.textContent, contains('Test Label')); + expect(result.getByTestId('test-input'), isNotNull); + + result.unmount(); + }); + + test('labelEl creates label element', () { + final label = labelEl('My Label'); + + final result = render(label); + + expect(result.container.textContent, equals('My Label')); + + result.unmount(); + }); + + test('getInputValue extracts value from input event', () async { + final component = registerFunctionComponent((props) { + final state = useState(''); + return div( + children: [ + input( + type: 'text', + value: state.value, + onChange: (e) => state.set(getInputValue(e).toDart), + props: {'data-testid': 'input'}, + ), + span(state.value, props: {'data-testid': 'output'}), + ], + ); + }); + + final result = render(fc(component)); + + await userType(result.getByTestId('input'), 'hello'); + + expect(result.getByTestId('output').textContent, equals('hello')); + + result.unmount(); + }); + }); + + group('Task Components - buildStats', () { + test('shows completion stats for tasks', () { + final tasks = [ + createMockTask({'id': '1', 'completed': true}), + createMockTask({'id': '2', 'completed': false}), + createMockTask({'id': '3', 'completed': true}), + ]; + + final stats = buildStats(tasks); + final result = render(stats); + + expect(result.container.textContent, contains('2/3 completed')); + + result.unmount(); + }); + + test('shows 0/0 for empty list', () { + final stats = buildStats([]); + final result = render(stats); + + expect(result.container.textContent, contains('0/0 completed')); + + result.unmount(); + }); + + test('shows 100% for all completed', () { + final tasks = [ + createMockTask({'id': '1', 'completed': true}), + createMockTask({'id': '2', 'completed': true}), + ]; + + final stats = buildStats(tasks); + final result = render(stats); + + expect(result.container.textContent, contains('2/2 completed')); + + result.unmount(); + }); + }); + + group('Task Components - buildTaskList', () { + test('shows empty state when no tasks', () { + final list = buildTaskList([], (a, b) {}, (c) {}); + + final wrapper = div(children: list); + final result = render(wrapper); + + expect(result.container.textContent, contains('No tasks yet')); + + result.unmount(); + }); + + test('renders task items', () { + final tasks = [ + createMockTask({'id': '1', 'title': 'Task One', 'completed': false}), + createMockTask({'id': '2', 'title': 'Task Two', 'completed': true}), + ]; + + final list = buildTaskList(tasks, (a, b) {}, (c) {}); + final wrapper = div(children: list); + final result = render(wrapper); + + expect(result.container.textContent, contains('Task One')); + expect(result.container.textContent, contains('Task Two')); + + result.unmount(); + }); + }); + + group('Task Components - buildTaskItem', () { + test('shows title and description', () { + final task = createMockTask({ + 'id': '123', + 'title': 'My Task', + 'description': 'Task description', + 'completed': false, + }); + + final item = buildTaskItem(task, (a, b) {}, (c) {}); + final result = render(item); + + expect(result.container.textContent, contains('My Task')); + expect(result.container.textContent, contains('Task description')); + + result.unmount(); + }); + + test('calls onToggle when checkbox clicked', () { + final task = createMockTask({ + 'id': 'task-1', + 'title': 'Toggle Test', + 'completed': false, + }); + + String? toggledId; + bool? toggledCompleted; + + final item = buildTaskItem(task, (id, completed) { + toggledId = id; + toggledCompleted = completed; + }, (c) {}); + final result = render(item); + + final checkbox = result.container.querySelector('.task-checkbox'); + expect(checkbox, isNotNull); + fireClick(checkbox!); + + expect(toggledId, equals('task-1')); + expect(toggledCompleted, isFalse); + + result.unmount(); + }); + + test('calls onDelete when delete button clicked', () { + final task = createMockTask({ + 'id': 'task-2', + 'title': 'Delete Test', + 'completed': false, + }); + + String? deletedId; + + final item = buildTaskItem(task, (a, b) {}, (id) => deletedId = id); + final result = render(item); + + final deleteBtn = result.container.querySelector('.btn-delete'); + expect(deleteBtn, isNotNull); + fireClick(deleteBtn!); + + expect(deletedId, equals('task-2')); + + result.unmount(); + }); + + test('shows checkmark when completed', () { + final task = createMockTask({ + 'id': '1', + 'title': 'Completed Task', + 'completed': true, + }); + + final item = buildTaskItem(task, (a, b) {}, (c) {}); + final result = render(item); + + expect(result.container.textContent, contains('\u2713')); + + result.unmount(); + }); + + test('applies completed class when task is done', () { + final task = createMockTask({ + 'id': '1', + 'title': 'Done Task', + 'completed': true, + }); + + final item = buildTaskItem(task, (a, b) {}, (c) {}); + final result = render(item); + + final taskDiv = result.container.querySelector('.task-item'); + expect(taskDiv, isNotNull); + final className = taskDiv!.className; + expect(className, contains('completed')); + + result.unmount(); + }); + }); + + group('handleTaskEvent', () { + test('task_created adds task to list', () { + final component = registerFunctionComponent((props) { + final tasksState = useStateJSArray([].toJS); + + return div( + children: [ + span( + 'Count: ${tasksState.value.length}', + props: {'data-testid': 'count'}, + ), + button( + text: 'Add', + onClick: () { + final newTask = createMockTask({'id': 'new-1', 'title': 'New'}); + tasksState.setWithUpdater( + (current) => + handleTaskEvent('task_created', newTask, current), + ); + }, + props: {'data-testid': 'add-btn'}, + ), + ], + ); + }); + + final result = render(fc(component)); + + expect(result.getByTestId('count').textContent, equals('Count: 0')); + + fireClick(result.getByTestId('add-btn')); + + expect(result.getByTestId('count').textContent, equals('Count: 1')); + + result.unmount(); + }); + + test('task_deleted removes task from list', () { + final component = registerFunctionComponent((props) { + final tasksState = useStateJSArray( + [ + createMockTask({'id': 'task-1', 'title': 'First'}), + createMockTask({'id': 'task-2', 'title': 'Second'}), + ].toJS, + ); + + return div( + children: [ + span( + 'Count: ${tasksState.value.length}', + props: {'data-testid': 'count'}, + ), + button( + text: 'Delete', + onClick: () { + final toDelete = createMockTask({'id': 'task-1'}); + tasksState.setWithUpdater( + (current) => + handleTaskEvent('task_deleted', toDelete, current), + ); + }, + props: {'data-testid': 'delete-btn'}, + ), + ], + ); + }); + + final result = render(fc(component)); + + expect(result.getByTestId('count').textContent, equals('Count: 2')); + + fireClick(result.getByTestId('delete-btn')); + + expect(result.getByTestId('count').textContent, equals('Count: 1')); + + result.unmount(); + }); + + test('task_updated replaces task in list', () { + final component = registerFunctionComponent((props) { + final tasksState = useStateJSArray( + [ + createMockTask({ + 'id': 'task-1', + 'title': 'Original', + 'completed': false, + }), + ].toJS, + ); + + String getTitle() { + final task = tasksState.value.first; + return task.title; + } + + return div( + children: [ + span(getTitle(), props: {'data-testid': 'title'}), + button( + text: 'Update', + onClick: () { + final updated = createMockTask({ + 'id': 'task-1', + 'title': 'Updated', + 'completed': true, + }); + tasksState.setWithUpdater( + (current) => + handleTaskEvent('task_updated', updated, current), + ); + }, + props: {'data-testid': 'update-btn'}, + ), + ], + ); + }); + + final result = render(fc(component)); + + expect(result.getByTestId('title').textContent, equals('Original')); + + fireClick(result.getByTestId('update-btn')); + + expect(result.getByTestId('title').textContent, equals('Updated')); + + result.unmount(); + }); + + test('unknown event type leaves list unchanged', () { + final component = registerFunctionComponent((props) { + final tasksState = useStateJSArray( + [ + createMockTask({'id': '1', 'title': 'Task'}), + ].toJS, + ); + + return div( + children: [ + span( + 'Count: ${tasksState.value.length}', + props: {'data-testid': 'count'}, + ), + button( + text: 'Unknown', + onClick: () { + final task = createMockTask({'id': '99'}); + tasksState.setWithUpdater( + (current) => handleTaskEvent('unknown_event', task, current), + ); + }, + props: {'data-testid': 'unknown-btn'}, + ), + ], + ); + }); + + final result = render(fc(component)); + + expect(result.getByTestId('count').textContent, equals('Count: 1')); + + fireClick(result.getByTestId('unknown-btn')); + + expect(result.getByTestId('count').textContent, equals('Count: 1')); + + result.unmount(); + }); + }); + + group('Form Helpers - Error Cases', () { + test('getInputValue throws when value is not a string', () { + final event = JSObject(); + final target = JSObject(); + target['value'] = 123.toJS; + event['target'] = target; + + expect(() => getInputValue(event), throwsStateError); + }); + + test('getInputValue throws when target is not an object', () { + final event = JSObject(); + event['target'] = null; + + expect(() => getInputValue(event), throwsStateError); + }); + }); +} diff --git a/examples/frontend/test/web_app_test.dart b/examples/frontend/test/web_app_test.dart new file mode 100644 index 0000000..bb3c707 --- /dev/null +++ b/examples/frontend/test/web_app_test.dart @@ -0,0 +1,528 @@ +/// UI interaction tests for the TaskFlow web app. +/// +/// Tests verify actual user interactions using the real lib/ components. +/// Run with: dart test -p chrome +@TestOn('browser') +library; + +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +import 'package:dart_node_react/src/testing_library.dart'; +import 'package:frontend/src/websocket.dart'; +import 'package:nadz/nadz.dart'; +import 'package:test/test.dart'; + +import '../web/app.dart' show App; +import 'test_helpers.dart'; + +void main() { + setUp(mockWebSocket); + + test('complete login flow - success', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'test-token-123', + 'user': {'name': 'Test User', 'email': 'test@example.com'}, + }, + }, + '/tasks': {'success': true, 'data': >[]}, + }); + + final result = render(App(fetchFn: mockFetch)); + + expect(result.container.textContent, contains('Sign In')); + + final emailInput = result.container.querySelector('input[type="email"]'); + final passInput = result.container.querySelector('input[type="password"]'); + await userType(emailInput!, 'test@example.com'); + await userType(passInput!, 'password123'); + + fireClick(result.container.querySelector('.btn-primary')!); + + await waitForText(result, 'Your Tasks'); + expect(result.container.textContent, contains('Welcome, Test User')); + + result.unmount(); + }); + + test('complete login flow - error', () async { + final mockFetch = createMockFetch({ + '/auth/login': {'success': false, 'error': 'Invalid credentials'}, + }); + + final result = render(App(fetchFn: mockFetch)); + + await userType( + result.container.querySelector('input[type="email"]')!, + 'bad@email.com', + ); + await userType( + result.container.querySelector('input[type="password"]')!, + 'wrong', + ); + + fireClick(result.container.querySelector('.btn-primary')!); + + await waitForText(result, 'Invalid credentials'); + // Verify error message div is rendered (line 67 coverage) + expect(result.container.querySelector('.error-msg'), isNotNull); + expect(result.container.textContent, contains('Sign In')); + + result.unmount(); + }); + + test('login -> view tasks -> logout', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': { + 'success': true, + 'data': [ + {'id': '1', 'title': 'Task One', 'completed': false}, + {'id': '2', 'title': 'Task Two', 'completed': true}, + ], + }, + }); + + final result = render(App(fetchFn: mockFetch)); + + await userType( + result.container.querySelector('input[type="email"]')!, + 'a@b.com', + ); + await userType( + result.container.querySelector('input[type="password"]')!, + 'pass', + ); + fireClick(result.container.querySelector('.btn-primary')!); + + await waitForText(result, 'Task One'); + expect(result.container.textContent, contains('Task Two')); + expect(result.container.textContent, contains('1/2 completed')); + + fireClick(result.container.querySelector('.btn-ghost')!); + + await waitForText(result, 'Sign In'); + expect(result.container.textContent, isNot(contains('Welcome'))); + + result.unmount(); + }); + + test('login -> add new task (no duplicates from websocket)', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': {'success': true, 'data': >[]}, + 'POST /tasks': { + 'success': true, + 'data': {'id': '99', 'title': 'My New Task', 'completed': false}, + }, + }); + + final result = render(App(fetchFn: mockFetch)); + + // Login + await userType( + result.container.querySelector('input[type="email"]')!, + 'a@b.com', + ); + await userType( + result.container.querySelector('input[type="password"]')!, + 'pass', + ); + fireClick(result.container.querySelector('.btn-primary')!); + + await waitForText(result, 'Your Tasks'); + // Verify 0 tasks initially + expect(result.container.textContent, contains('0/0 completed')); + + // Add a new task + final taskInput = result.container.querySelector( + 'input[placeholder="What needs to be done?"]', + )!; + await userType(taskInput, 'My New Task'); + + // Click Add Task button + final addButton = result.container.querySelector('.btn-primary')!; + fireClick(addButton); + + // The new task should appear in the list + await waitForText(result, 'My New Task'); + + // Simulate WebSocket also sending task_created (what the server does!) + simulateWsMessage( + '{"type":"task_created","data":' + '{"id":"99","title":"My New Task","completed":false}}', + ); + + // Give React time to process the WebSocket event + await Future.delayed(const Duration(milliseconds: 100)); + + // CRITICAL: Assert EXACTLY ONE task, not duplicates! + final taskItems = result.container.querySelectorAll('.task-item'); + expect( + taskItems.length, + 1, + reason: 'Should have exactly 1 task, not duplicates', + ); + expect(result.container.textContent, contains('0/1 completed')); + + result.unmount(); + }); + + test('add task - WS arrives BEFORE HTTP response (race condition)', () async { + // This tests the REAL bug: WebSocket is faster than HTTP response! + // Server broadcasts task_created immediately, HTTP response is slower. + + // Create fetch that triggers WS message BEFORE returning HTTP response + Future> racingFetch( + String url, { + String method = 'GET', + String? token, + Map? body, + }) async { + if (url.contains('/auth/login')) { + return Success( + createJSObject({ + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }), + ); + } + if (url.contains('/tasks') && method == 'GET') { + return Success( + createJSObject({'success': true, 'data': >[]}), + ); + } + if (url.contains('/tasks') && method == 'POST') { + // SIMULATE RACE: WebSocket arrives BEFORE HTTP response! + simulateWsMessage( + '{"type":"task_created","data":' + '{"id":"race-1","title":"Race Task","completed":false}}', + ); + // Small delay, then HTTP returns + await Future.delayed(const Duration(milliseconds: 50)); + return Success( + createJSObject({ + 'success': true, + 'data': {'id': 'race-1', 'title': 'Race Task', 'completed': false}, + }), + ); + } + throw StateError('No mock for $method $url'); + } + + final result = render(App(fetchFn: racingFetch)); + + // Login + await userType( + result.container.querySelector('input[type="email"]')!, + 'a@b.com', + ); + await userType( + result.container.querySelector('input[type="password"]')!, + 'pass', + ); + fireClick(result.container.querySelector('.btn-primary')!); + + await waitForText(result, 'Your Tasks'); + + // Add task - WS will arrive before HTTP! + final taskInput = result.container.querySelector( + 'input[placeholder="What needs to be done?"]', + )!; + await userType(taskInput, 'Race Task'); + fireClick(result.container.querySelector('.btn-primary')!); + + // Wait for task to appear + await waitForText(result, 'Race Task'); + await Future.delayed(const Duration(milliseconds: 200)); + + // CRITICAL: Must have exactly 1 task, not 2! + final taskItems = result.container.querySelectorAll('.task-item'); + expect( + taskItems.length, + 1, + reason: 'WS arrived first, HTTP should NOT add duplicate!', + ); + + result.unmount(); + }); + + test('switch between login and register views', () { + final result = render(App()); + + expect(result.container.textContent, contains('Sign In')); + + fireClick(result.container.querySelector('.btn-link')!); + + expect(result.container.textContent, contains('Create Account')); + expect(result.container.textContent, contains('Name')); + + fireClick(result.container.querySelector('.btn-link')!); + + expect(result.container.textContent, contains('Sign In')); + + result.unmount(); + }); + + test('register flow - success', () async { + final mockFetch = createMockFetch({ + '/auth/register': { + 'success': true, + 'data': { + 'token': 'new-token', + 'user': {'name': 'New User'}, + }, + }, + '/tasks': {'success': true, 'data': >[]}, + }); + + final result = render(App(fetchFn: mockFetch)); + + fireClick(result.container.querySelector('.btn-link')!); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'New User'); + await userType(inputs[1], 'new@user.com'); + await userType(inputs[2], 'password'); + + fireClick(result.container.querySelector('.btn-primary')!); + + await waitForText(result, 'Your Tasks'); + expect(result.container.textContent, contains('Welcome, New User')); + + result.unmount(); + }); + + test('login with no token in response', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'user': {'name': 'No Token User'}, + }, + }, + }); + + final result = render(App(fetchFn: mockFetch)); + + await userType( + result.container.querySelector('input[type="email"]')!, + 'x@y.com', + ); + await userType( + result.container.querySelector('input[type="password"]')!, + 'pass', + ); + fireClick(result.container.querySelector('.btn-primary')!); + + await waitForText(result, 'No token'); + + result.unmount(); + }); + + test('login with null data in response', () async { + final mockFetch = createMockFetch({ + '/auth/login': {'success': true, 'data': null}, + }); + + final result = render(App(fetchFn: mockFetch)); + + await userType( + result.container.querySelector('input[type="email"]')!, + 'x@y.com', + ); + await userType( + result.container.querySelector('input[type="password"]')!, + 'pass', + ); + fireClick(result.container.querySelector('.btn-primary')!); + + await waitForText(result, 'Login failed'); + + result.unmount(); + }); + + test('register with no token in response', () async { + final mockFetch = createMockFetch({ + '/auth/register': { + 'success': true, + 'data': { + 'user': {'name': 'No Token'}, + }, + }, + }); + + final result = render(App(fetchFn: mockFetch)); + + fireClick(result.container.querySelector('.btn-link')!); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'Name'); + await userType(inputs[1], 'a@b.com'); + await userType(inputs[2], 'pass'); + + fireClick(result.container.querySelector('.btn-primary')!); + + await waitForText(result, 'No token'); + + result.unmount(); + }); + + test('register with null data in response', () async { + final mockFetch = createMockFetch({ + '/auth/register': {'success': true, 'data': null}, + }); + + final result = render(App(fetchFn: mockFetch)); + + fireClick(result.container.querySelector('.btn-link')!); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'Name'); + await userType(inputs[1], 'a@b.com'); + await userType(inputs[2], 'pass'); + + fireClick(result.container.querySelector('.btn-primary')!); + + await waitForText(result, 'Registration failed'); + + result.unmount(); + }); + + test('register with server error', () async { + final mockFetch = createMockFetch({ + '/auth/register': {'success': false, 'error': 'Email already exists'}, + }); + + final result = render(App(fetchFn: mockFetch)); + + fireClick(result.container.querySelector('.btn-link')!); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'Name'); + await userType(inputs[1], 'existing@email.com'); + await userType(inputs[2], 'pass'); + + fireClick(result.container.querySelector('.btn-primary')!); + + await waitForText(result, 'Email already exists'); + + result.unmount(); + }); + + test('login with fetch exception', () async { + // Create a fetch that throws to test catchError branch + final throwingFetch = createThrowingFetch(); + + final result = render(App(fetchFn: throwingFetch)); + + await userType( + result.container.querySelector('input[type="email"]')!, + 'x@y.com', + ); + await userType( + result.container.querySelector('input[type="password"]')!, + 'pass', + ); + fireClick(result.container.querySelector('.btn-primary')!); + + await waitForText(result, 'Network error'); + + result.unmount(); + }); + + test('register with fetch exception', () async { + final throwingFetch = createThrowingFetch(); + + final result = render(App(fetchFn: throwingFetch)); + + fireClick(result.container.querySelector('.btn-link')!); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'Name'); + await userType(inputs[1], 'a@b.com'); + await userType(inputs[2], 'pass'); + + fireClick(result.container.querySelector('.btn-primary')!); + + await waitForText(result, 'Network error'); + + result.unmount(); + }); + + // --- WebSocket Tests --- + + test('handleWebSocketMessage parses JSON and calls callback', () { + final events = []; + handleWebSocketMessage('{"type":"created","data":{"id":"1"}}', events.add); + + expect(events.length, 1); + expect((events[0]['type']! as JSString).toDart, 'created'); + }); + + test('connectWebSocket creates websocket with handlers', () { + var openCalled = false; + var closeCalled = false; + final events = []; + + final ws = connectWebSocket( + token: 'test-token', + onTaskEvent: events.add, + onOpen: () => openCalled = true, + onClose: () => closeCalled = true, + ); + + expect(ws, isNotNull); + + // Trigger the handlers manually via the mock + final dummyEvent = JSObject(); + ws!.onopen?.callAsFunction(null, dummyEvent); + expect(openCalled, isTrue); + + ws.onclose?.callAsFunction(null, dummyEvent); + expect(closeCalled, isTrue); + + // Test message handler with valid JSON string + final messageEvent = JSObject(); + messageEvent['data'] = '{"type":"test"}'.toJS; + ws.onmessage?.callAsFunction(null, messageEvent); + expect(events.length, 1); + + // Test error handler (should not throw) + ws.onerror?.callAsFunction(null, dummyEvent); + + ws.close(); + }); + + test('connectWebSocket handles non-string message data', () { + final events = []; + + final ws = connectWebSocket(token: 'test-token', onTaskEvent: events.add); + + // Send non-string data (should be ignored) + final messageEvent = JSObject(); + messageEvent['data'] = 123.toJS; + ws!.onmessage?.callAsFunction(null, messageEvent); + + expect(events, isEmpty); + ws.close(); + }); +} diff --git a/examples/frontend/web/app.dart b/examples/frontend/web/app.dart index 2843f09..203373e 100644 --- a/examples/frontend/web/app.dart +++ b/examples/frontend/web/app.dart @@ -1,13 +1,15 @@ import 'dart:async'; import 'dart:js_interop'; import 'dart:js_interop_unsafe'; + import 'package:dart_node_react/dart_node_react.dart'; +import 'package:frontend/frontend.dart'; import 'package:nadz/nadz.dart'; import 'package:shared/http/http_client.dart'; -import 'websocket.dart'; - -const apiUrl = 'http://localhost:3000'; +/// Creates a SetToken function that converts JSString to String for storage +SetToken _createSetToken(StateHook tokenState) => + (t) => tokenState.set(t?.toDart); void main() { final root = Document.getElementById('root'); @@ -18,31 +20,45 @@ void main() { // React component functions follow PascalCase naming convention // ignore: non_constant_identifier_names -ReactElement App() => createElement( +ReactElement App({Fetch? fetchFn}) => createElement( ((JSAny props) { - final (tokenState, setToken) = useState(null); - final (userState, setUser) = useState(null); - final (viewState, setView) = useState('login'.toJS); - - final token = tokenState as JSString?; - final user = userState as JSObject?; - final view = (viewState as JSString?)?.toDart ?? 'login'; + final tokenState = useState(null); + final userState = useStateJS(null); + final viewState = useState('login'); + final doFetch = fetchFn ?? fetch; + + final auth = ( + setToken: _createSetToken(tokenState), + setUser: userState.set, + setView: viewState.set, + ); return div( className: 'app', children: [ - _buildHeader(user, () { - setToken.callAsFunction(); - setUser.callAsFunction(); - setView.callAsFunction(null, 'login'.toJS); - }), + buildHeader( + switch (userState.value) { + final JSObject user => user, + _ => null, + }, + () { + tokenState.set(null); + userState.set(null); + viewState.set('login'); + }, + ), mainEl( className: 'main-content', - child: (token == null) - ? (view == 'register') - ? _buildRegisterForm(setToken, setUser, setView) - : _buildLoginForm(setToken, setUser, setView) - : _buildTaskManager(token.toDart, setToken, setUser, setView), + child: (tokenState.value == null) + ? (viewState.value == 'register') + ? buildRegisterForm(auth, fetchFn: doFetch) + : buildLoginForm(auth, fetchFn: doFetch) + : _buildTaskManager( + tokenState.value!, + userState, + viewState, + doFetch, + ), ), footer( className: 'footer', @@ -53,432 +69,147 @@ ReactElement App() => createElement( }).toJS, ); -HeaderElement _buildHeader(JSObject? user, void Function() onLogout) { - final userName = user?['name']?.toString(); - return header( - className: 'header', - children: [ - div( - className: 'header-content', - children: [ - h1('TaskFlow', className: 'logo'), - if (userName != null) - div( - className: 'user-info', - children: [ - span('Welcome, $userName', className: 'user-name'), - button( - text: 'Logout', - className: 'btn btn-ghost', - onClick: onLogout, - ), - ], - ) - else - span('', className: 'spacer'), - ], - ), - ], - ); -} - -ReactElement _buildLoginForm( - JSFunction setToken, - JSFunction setUser, - JSFunction setView, +ReactElement _buildTaskManager( + String token, + StateHookJS userState, + StateHook viewState, + Fetch doFetch, ) => createElement( ((JSAny props) { - final (emailState, setEmail) = useState(''.toJS); - final (passState, setPass) = useState(''.toJS); - final (errorState, setError) = useState(null); - final (loadingState, setLoading) = useState(false.toJS); - - final email = (emailState as JSString?)?.toDart ?? ''; - final password = (passState as JSString?)?.toDart ?? ''; - final error = (errorState as JSString?)?.toDart; - final loading = (loadingState as JSBoolean?)?.toDart ?? false; - - void handleSubmit() { - setLoading.callAsFunction(null, true.toJS); - setError.callAsFunction(); + final tasksState = useStateJSArray(null); + final newTaskState = useState(''); + final descState = useState(''); + final loadingState = useState(true); + final errorState = useState(null); + // Fetch tasks on mount + useEffect(() { unawaited( - fetchJson( - '$apiUrl/auth/login', - method: 'POST', - body: {'email': email, 'password': password}, - ) + doFetch('$apiUrl/tasks', token: token) .then((result) { result.match( onSuccess: (response) { - final data = response['data']; - switch (data) { - case null: - setError.callAsFunction(null, 'Login failed'.toJS); - case final JSObject details: - switch (details['token']) { - case final JSString token: - setToken.callAsFunction(null, token); - default: - setError.callAsFunction(null, 'No token'.toJS); - } - setUser.callAsFunction(null, details['user']); + switch (response['data']) { + case final JSArray tasks: + final taskList = [ + for (final item in tasks.toDart) + if (item case final JSObject task) + JSTask.fromJS(task), + ]; + tasksState.set(taskList); + errorState.set(null); + default: + tasksState.set([]); } }, - onError: (message) => - setError.callAsFunction(null, message.toJS), + onError: errorState.set, ); }) .catchError((Object e) { - setError.callAsFunction(null, e.toString().toJS); + errorState.set(e.toString()); }) - .whenComplete(() => setLoading.callAsFunction(null, false.toJS)), + .whenComplete(() => loadingState.set(false)), ); - } - - return div( - className: 'auth-card', - children: [ - h2('Sign In', className: 'auth-title'), - if (error != null) - div(className: 'error-msg', child: span(error)) - else - span(''), - div( - className: 'form-group', - children: [ - _labelEl('Email'), - input( - type: 'email', - placeholder: 'you@example.com', - value: email, - className: 'input', - onChange: (e) => setEmail.callAsFunction(null, _getInputValue(e)), - ), - ], - ), - div( - className: 'form-group', - children: [ - _labelEl('Password'), - input( - type: 'password', - placeholder: '••••••••', - value: password, - className: 'input', - onChange: (e) => setPass.callAsFunction(null, _getInputValue(e)), - ), - ], - ), - button( - text: loading ? 'Signing in...' : 'Sign In', - className: 'btn btn-primary btn-full', - onClick: loading ? null : handleSubmit, - ), - div( - className: 'auth-footer', - children: [ - span("Don't have an account? "), - button( - text: 'Register', - className: 'btn-link', - onClick: () => setView.callAsFunction(null, 'register'.toJS), - ), - ], - ), - ], - ); - }).toJS, -); - -ReactElement _buildRegisterForm( - JSFunction setToken, - JSFunction setUser, - JSFunction setView, -) => createElement( - ((JSAny props) { - final (nameState, setName) = useState(''.toJS); - final (emailState, setEmail) = useState(''.toJS); - final (passState, setPass) = useState(''.toJS); - final (errorState, setError) = useState(null); - final (loadingState, setLoading) = useState(false.toJS); - - final name = (nameState as JSString?)?.toDart ?? ''; - final email = (emailState as JSString?)?.toDart ?? ''; - final password = (passState as JSString?)?.toDart ?? ''; - final error = (errorState as JSString?)?.toDart; - final loading = (loadingState as JSBoolean?)?.toDart ?? false; - - void handleSubmit() { - setLoading.callAsFunction(null, true.toJS); - setError.callAsFunction(); + return null; + }, []); - unawaited( - fetchJson( - '$apiUrl/auth/register', - method: 'POST', - body: {'email': email, 'password': password, 'name': name}, - ) - .then((result) { - result.match( - onSuccess: (response) { - final data = response['data']; - switch (data) { - case null: - setError.callAsFunction(null, 'Registration failed'.toJS); - case final JSObject details: - switch (details['token']) { - case final JSString token: - setToken.callAsFunction(null, token); - default: - setError.callAsFunction(null, 'No token'.toJS); - } - setUser.callAsFunction(null, details['user']); - } - }, - onError: (message) => - setError.callAsFunction(null, message.toJS), + // WebSocket connection for real-time updates + useEffect(() { + final ws = connectWebSocket( + token: token, + onTaskEvent: (event) { + final type = switch (event['type']) { + final JSString t => t.toDart, + _ => null, + }; + switch (event['data']) { + case final JSObject data: + tasksState.setWithUpdater( + (current) => + handleTaskEvent(type, JSTask.fromJS(data), current), ); - }) - .catchError((Object e) { - setError.callAsFunction(null, e.toString().toJS); - }) - .whenComplete(() => setLoading.callAsFunction(null, false.toJS)), + default: + break; + } + }, ); - } - - return div( - className: 'auth-card', - children: [ - h2('Create Account', className: 'auth-title'), - if (error != null) - div(className: 'error-msg', child: span(error)) - else - span(''), - div( - className: 'form-group', - children: [ - _labelEl('Name'), - input( - type: 'text', - placeholder: 'Your name', - value: name, - className: 'input', - onChange: (e) => setName.callAsFunction(null, _getInputValue(e)), - ), - ], - ), - div( - className: 'form-group', - children: [ - _labelEl('Email'), - input( - type: 'email', - placeholder: 'you@example.com', - value: email, - className: 'input', - onChange: (e) => setEmail.callAsFunction(null, _getInputValue(e)), - ), - ], - ), - div( - className: 'form-group', - children: [ - _labelEl('Password'), - input( - type: 'password', - placeholder: '••••••••', - value: password, - className: 'input', - onChange: (e) => setPass.callAsFunction(null, _getInputValue(e)), - ), - ], - ), - button( - text: loading ? 'Creating...' : 'Create Account', - className: 'btn btn-primary btn-full', - onClick: loading ? null : handleSubmit, - ), - div( - className: 'auth-footer', - children: [ - span('Already have an account? '), - button( - text: 'Sign In', - className: 'btn-link', - onClick: () => setView.callAsFunction(null, 'login'.toJS), - ), - ], - ), - ], - ); - }).toJS, -); - -ReactElement _buildTaskManager( - String token, - JSFunction setToken, - JSFunction setUser, - JSFunction setView, -) => createElement( - ((JSAny props) { - final (tasksState, setTasks) = useState([].toJS); - final (newTaskState, setNewTask) = useState(''.toJS); - final (descState, setDesc) = useState(''.toJS); - final (loadingState, setLoading) = useState(true.toJS); - final (errorState, setError) = useState(null); - - final tasks = tasksState as JSArray?; - final newTask = (newTaskState as JSString?)?.toDart ?? ''; - final desc = (descState as JSString?)?.toDart ?? ''; - final loading = (loadingState as JSBoolean?)?.toDart ?? false; - final error = (errorState as JSString?)?.toDart; - - // Fetch tasks on mount - useEffect( - (() { - unawaited( - fetchTasks(token: token, apiUrl: apiUrl) - .then((result) { - result.match( - onSuccess: (list) { - setTasks.callAsFunction(null, list); - setError.callAsFunction(); - }, - onError: (message) => - setError.callAsFunction(null, message.toJS), - ); - }) - .catchError((Object e) { - setError.callAsFunction(null, e.toString().toJS); - }) - .whenComplete(() => setLoading.callAsFunction(null, false.toJS)), - ); - }).toJS, - [].toJS, - ); - - // WebSocket connection for real-time updates - useEffect( - (() { - final ws = connectWebSocket( - token: token, - onTaskEvent: (event) { - final type = (event['type'] as JSString?)?.toDart; - final data = event['data'] as JSObject?; - switch (data) { - case final JSObject d: - _handleTaskEvent(type, d, setTasks); - case null: - break; - } - }, - ); - return (() => ws?.close()).toJS; - }).toJS, - [token.toJS].toJS, - ); + return () => ws?.close(); + }, [token]); void addTask() { - switch (newTask.trim().isEmpty) { + switch (newTaskState.value.trim().isEmpty) { case true: return; case false: - setError.callAsFunction(); + errorState.set(null); unawaited( - fetchJson( - '$apiUrl/tasks', - method: 'POST', - token: token, - body: {'title': newTask, 'description': desc}, - ) - .then((result) { - result.match( - onSuccess: (response) { - final task = response['data']; - switch (task) { - case final JSObject created: - final current = (tasks?.toDart ?? []).cast(); - setTasks.callAsFunction( - null, - [...current, created].toJS, - ); - setNewTask.callAsFunction(null, ''.toJS); - setDesc.callAsFunction(null, ''.toJS); - default: - setError.callAsFunction( - null, - 'Invalid task payload'.toJS, - ); - } - }, - onError: (message) => - setError.callAsFunction(null, message.toJS), - ); - }) - .catchError((Object e) { - setError.callAsFunction(null, e.toString().toJS); - }), - ); - } - } - - void toggleTask(String id, bool completed) { - unawaited( - fetchJson( - '$apiUrl/tasks/$id', - method: 'PUT', + doFetch( + '$apiUrl/tasks', + method: 'POST', token: token, - body: {'completed': !completed}, - ) - .then((result) { + body: { + 'title': newTaskState.value, + 'description': descState.value, + }, + ).then((result) { result.match( onSuccess: (response) { - final updated = response['data']; - switch (updated) { - case final JSObject task: - final current = (tasks?.toDart ?? []).cast(); - final newList = current.map((t) { - final taskId = (t['id'] as JSString?)?.toDart; - return (taskId == id) ? task : t; - }).toList(); - setTasks.callAsFunction(null, newList.toJS); - default: - setError.callAsFunction( - null, - 'Invalid task payload'.toJS, + switch (response['data']) { + case final JSObject created: + final newTask = JSTask.fromJS(created); + tasksState.setWithUpdater( + (prev) => addTaskIfNotExists(prev, newTask), ); + newTaskState.set(''); + descState.set(''); + default: + errorState.set('Invalid task payload'); } }, - onError: (message) => - setError.callAsFunction(null, message.toJS), + onError: errorState.set, ); - }) - .catchError((Object e) { - setError.callAsFunction(null, e.toString().toJS); }), + ); + } + } + + void toggleTask(String id, bool completed) { + unawaited( + doFetch( + '$apiUrl/tasks/$id', + method: 'PUT', + token: token, + body: {'completed': !completed}, + ).then((result) { + result.match( + onSuccess: (response) { + switch (response['data']) { + case final JSObject updatedTask: + tasksState.setWithUpdater( + (prev) => updateTaskById(prev, JSTask.fromJS(updatedTask)), + ); + default: + errorState.set('Invalid task payload'); + } + }, + onError: errorState.set, + ); + }), ); } void deleteTask(String id) { unawaited( - fetchJson('$apiUrl/tasks/$id', method: 'DELETE', token: token) - .then((result) { - result.match( - onSuccess: (_) { - final current = (tasks?.toDart ?? []).cast(); - final newList = current - .where((t) => (t['id'] as JSString?)?.toDart != id) - .toList(); - setTasks.callAsFunction(null, newList.toJS); - }, - onError: (message) => - setError.callAsFunction(null, message.toJS), - ); - }) - .catchError((Object e) { - setError.callAsFunction(null, e.toString().toJS); - }), + doFetch('$apiUrl/tasks/$id', method: 'DELETE', token: token).then(( + result, + ) { + result.match( + onSuccess: (_) { + tasksState.setWithUpdater((prev) => removeTaskById(prev, id)); + }, + onError: errorState.set, + ); + }), ); } @@ -489,7 +220,7 @@ ReactElement _buildTaskManager( className: 'task-header', children: [ h2('Your Tasks', className: 'section-title'), - _buildStats(tasks), + buildStats(tasksState.value), ], ), div( @@ -501,18 +232,16 @@ ReactElement _buildTaskManager( input( type: 'text', placeholder: 'What needs to be done?', - value: newTask, + value: newTaskState.value, className: 'input input-lg', - onChange: (e) => - setNewTask.callAsFunction(null, _getInputValue(e)), + onChange: (e) => newTaskState.set(getInputValue(e).toDart), ), input( type: 'text', placeholder: 'Description (optional)', - value: desc, + value: descState.value, className: 'input', - onChange: (e) => - setDesc.callAsFunction(null, _getInputValue(e)), + onChange: (e) => descState.set(getInputValue(e).toDart), ), button( text: '+ Add Task', @@ -523,145 +252,16 @@ ReactElement _buildTaskManager( ), ], ), - if (error != null) - div(className: 'error-msg', child: span(error)) - else if (loading) + if (errorState.value != null) + div(className: 'error-msg', child: span(errorState.value!)) + else if (loadingState.value) div(className: 'loading', child: span('Loading...')) else div( className: 'task-list', - children: _buildTaskList(tasks, toggleTask, deleteTask), + children: buildTaskList(tasksState.value, toggleTask, deleteTask), ), ], ); }).toJS, ); - -DivElement _buildStats(JSArray? tasks) { - final list = (tasks?.toDart ?? []).cast(); - final total = list.length; - final completed = list - .where((t) => (t['completed'] as JSBoolean?)?.toDart ?? false) - .length; - final pct = total > 0 ? (completed / total * 100).round() : 0; - return div( - className: 'stats', - children: [ - span('$completed/$total completed', className: 'stat-text'), - div( - className: 'progress-bar', - child: div( - className: 'progress-fill', - props: {'style': {'width': '$pct%'}.jsify()}, - ), - ), - ], - ); -} - -List _buildTaskList( - JSArray? tasks, - void Function(String, bool) onToggle, - void Function(String) onDelete, -) { - final list = (tasks?.toDart ?? []).cast(); - return list.isEmpty - ? [ - div( - className: 'empty-state', - children: [ - span('🎯', className: 'empty-icon'), - pEl('No tasks yet. Add one above!', className: 'empty-text'), - ], - ), - ] - : list.map((task) => _buildTaskItem(task, onToggle, onDelete)).toList(); -} - -DivElement _buildTaskItem( - JSObject task, - void Function(String, bool) onToggle, - void Function(String) onDelete, -) { - final id = (task['id'] as JSString?)?.toDart ?? ''; - final title = (task['title'] as JSString?)?.toDart ?? ''; - final description = (task['description'] as JSString?)?.toDart; - final completed = (task['completed'] as JSBoolean?)?.toDart ?? false; - final checkClass = completed ? 'task-checkbox completed' : 'task-checkbox'; - final titleClass = completed ? 'task-title completed' : 'task-title'; - final itemClass = completed ? 'task-item completed' : 'task-item'; - - return div( - className: itemClass, - children: [ - div( - className: checkClass, - props: {'onClick': ((JSAny? _) => onToggle(id, completed)).toJS}, - child: completed ? span('✓', className: 'check-icon') : span(''), - ), - div( - className: 'task-content', - children: [ - span(title, className: titleClass), - if (description != null && description.isNotEmpty) - span(description, className: 'task-desc') - else - span(''), - ], - ), - button( - text: '×', - className: 'btn-delete', - onClick: () => onDelete(id), - ), - ], - ); -} - -ReactElement _labelEl(String text) => createElement( - 'label'.toJS, - createProps({'className': 'label'}), - text.toJS, -); - -JSString _getInputValue(JSAny event) { - final obj = event as JSObject; - final target = obj['target']; - return switch (target) { - final JSObject t => switch (t['value']) { - final JSString v => v, - _ => throw StateError('Input value is not a string'), - }, - _ => throw StateError('Event target is not an object'), - }; -} - -/// Handle incoming WebSocket task events using functional updater -void _handleTaskEvent( - String? type, - JSObject task, - JSFunction setTasks, -) { - final taskId = (task['id'] as JSString?)?.toDart; - - // Use functional updater to get current state - final updater = ((JSAny? prevState) { - final tasks = prevState as JSArray?; - final current = (tasks?.toDart ?? []).cast(); - - return switch (type) { - 'task_created' => [...current, task].toJS, - 'task_updated' => current.map((t) { - final id = (t['id'] as JSString?)?.toDart; - return (id == taskId) ? task : t; - }).toList().toJS, - 'task_deleted' => current.where((t) { - final id = (t['id'] as JSString?)?.toDart; - return id != taskId; - }).toList().toJS, - _ => prevState, - }; - }).toJS; - - setTasks.callAsFunction(null, updater); -} diff --git a/examples/frontend/web/components/auth_forms.dart b/examples/frontend/web/components/auth_forms.dart deleted file mode 100644 index c758579..0000000 --- a/examples/frontend/web/components/auth_forms.dart +++ /dev/null @@ -1,221 +0,0 @@ -import 'dart:async'; -import 'dart:js_interop'; - -import 'package:dart_node_react/dart_node_react.dart'; -import 'package:nadz/nadz.dart'; -import 'package:shared/http/http_client.dart'; - -import '../types.dart'; -import 'form_helpers.dart'; - -/// Build login form component -ReactElement buildLoginForm(AuthEffects auth) => createElement( - ((JSAny props) { - final (emailState, setEmail) = useState(''.toJS); - final (passState, setPass) = useState(''.toJS); - final (errorState, setError) = useState(null); - final (loadingState, setLoading) = useState(false.toJS); - - final email = (emailState as JSString?)?.toDart ?? ''; - final password = (passState as JSString?)?.toDart ?? ''; - final error = (errorState as JSString?)?.toDart; - final loading = (loadingState as JSBoolean?)?.toDart ?? false; - - void handleSubmit() { - setLoading.callAsFunction(null, true.toJS); - setError.callAsFunction(); - - unawaited( - fetchJson( - '$apiUrl/auth/login', - method: 'POST', - body: {'email': email, 'password': password}, - ) - .then((result) { - result.match( - onSuccess: (response) { - final data = getProp(response, 'data') as JSObject?; - switch (data) { - case null: - setError.callAsFunction(null, 'Login failed'.toJS); - case final d: - switch (getProp(d, 'token')) { - case final JSString token: - auth.setToken(token); - default: - setError.callAsFunction(null, 'No token'.toJS); - } - auth.setUser(getProp(d, 'user') as JSObject?); - } - }, - onError: (message) => - setError.callAsFunction(null, message.toJS), - ); - }) - .catchError((Object e) { - setError.callAsFunction(null, e.toString().toJS); - }) - .whenComplete(() => setLoading.callAsFunction(null, false.toJS)), - ); - } - - return div( - className: 'auth-card', - children: [ - h2('Sign In', className: 'auth-title'), - if (error != null) - div(className: 'error-msg', child: span(error)) - else - span(''), - formGroup( - 'Email', - input( - type: 'email', - placeholder: 'you@example.com', - value: email, - className: 'input', - onChange: (e) => setEmail.callAsFunction(null, getInputValue(e)), - ), - ), - formGroup( - 'Password', - input( - type: 'password', - placeholder: '••••••••', - value: password, - className: 'input', - onChange: (e) => setPass.callAsFunction(null, getInputValue(e)), - ), - ), - button( - text: loading ? 'Signing in...' : 'Sign In', - className: 'btn btn-primary btn-full', - onClick: loading ? null : handleSubmit, - ), - div( - className: 'auth-footer', - children: [ - span("Don't have an account? "), - button( - text: 'Register', - className: 'btn-link', - onClick: () => auth.setView('register'), - ), - ], - ), - ], - ); - }).toJS, -); - -/// Build register form component -ReactElement buildRegisterForm(AuthEffects auth) => createElement( - ((JSAny props) { - final (nameState, setName) = useState(''.toJS); - final (emailState, setEmail) = useState(''.toJS); - final (passState, setPass) = useState(''.toJS); - final (errorState, setError) = useState(null); - final (loadingState, setLoading) = useState(false.toJS); - - final name = (nameState as JSString?)?.toDart ?? ''; - final email = (emailState as JSString?)?.toDart ?? ''; - final password = (passState as JSString?)?.toDart ?? ''; - final error = (errorState as JSString?)?.toDart; - final loading = (loadingState as JSBoolean?)?.toDart ?? false; - - void handleSubmit() { - setLoading.callAsFunction(null, true.toJS); - setError.callAsFunction(); - - unawaited( - fetchJson( - '$apiUrl/auth/register', - method: 'POST', - body: {'email': email, 'password': password, 'name': name}, - ) - .then((result) { - result.match( - onSuccess: (response) { - final data = getProp(response, 'data') as JSObject?; - switch (data) { - case null: - setError.callAsFunction(null, 'Registration failed'.toJS); - case final d: - switch (getProp(d, 'token')) { - case final JSString token: - auth.setToken(token); - default: - setError.callAsFunction(null, 'No token'.toJS); - } - auth.setUser(getProp(d, 'user') as JSObject?); - } - }, - onError: (message) => - setError.callAsFunction(null, message.toJS), - ); - }) - .catchError((Object e) { - setError.callAsFunction(null, e.toString().toJS); - }) - .whenComplete(() => setLoading.callAsFunction(null, false.toJS)), - ); - } - - return div( - className: 'auth-card', - children: [ - h2('Create Account', className: 'auth-title'), - if (error != null) - div(className: 'error-msg', child: span(error)) - else - span(''), - formGroup( - 'Name', - input( - type: 'text', - placeholder: 'Your name', - value: name, - className: 'input', - onChange: (e) => setName.callAsFunction(null, getInputValue(e)), - ), - ), - formGroup( - 'Email', - input( - type: 'email', - placeholder: 'you@example.com', - value: email, - className: 'input', - onChange: (e) => setEmail.callAsFunction(null, getInputValue(e)), - ), - ), - formGroup( - 'Password', - input( - type: 'password', - placeholder: '••••••••', - value: password, - className: 'input', - onChange: (e) => setPass.callAsFunction(null, getInputValue(e)), - ), - ), - button( - text: loading ? 'Creating...' : 'Create Account', - className: 'btn btn-primary btn-full', - onClick: loading ? null : handleSubmit, - ), - div( - className: 'auth-footer', - children: [ - span('Already have an account? '), - button( - text: 'Sign In', - className: 'btn-link', - onClick: () => auth.setView('login'), - ), - ], - ), - ], - ); - }).toJS, -); diff --git a/examples/frontend/web/components/form_helpers.dart b/examples/frontend/web/components/form_helpers.dart deleted file mode 100644 index 212122c..0000000 --- a/examples/frontend/web/components/form_helpers.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'dart:js_interop'; -import 'dart:js_interop_unsafe'; -import 'package:dart_node_react/dart_node_react.dart'; - -/// Create a form group with label -DivElement formGroup(String labelText, ReactElement inputElement) => div( - className: 'form-group', - children: [labelEl(labelText), inputElement], - ); - -/// Create a label element -ReactElement labelEl(String text) => createElement( - 'label'.toJS, - createProps({'className': 'label'}), - text.toJS, - ); - -/// Extract input value from event -JSString getInputValue(JSAny event) { - final obj = event as JSObject; - final target = obj.getProperty('target'.toJS); - return switch (target) { - final JSObject t => switch (t.getProperty('value'.toJS)) { - final JSString v => v, - _ => throw StateError('Input value is not a string'), - }, - _ => throw StateError('Event target is not an object'), - }; -} - -/// Get property from JSObject safely -JSAny? getProp(JSObject obj, String key) => obj.getProperty(key.toJS); - -/// Get string property -String? getStringProp(JSObject obj, String key) => - (obj.getProperty(key.toJS) as JSString?)?.toDart; - -/// Get bool property -bool getBoolProp(JSObject obj, String key, {bool defaultValue = false}) => - (obj.getProperty(key.toJS) as JSBoolean?)?.toDart ?? defaultValue; diff --git a/examples/frontend/web/components/header.dart b/examples/frontend/web/components/header.dart deleted file mode 100644 index 0946465..0000000 --- a/examples/frontend/web/components/header.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:js_interop'; -import 'dart:js_interop_unsafe'; -import 'package:dart_node_react/dart_node_react.dart'; -import '../types.dart'; - -/// Build app header component -JSObject buildHeader(JSObject? user, OnClick onLogout) { - final userName = user?.getProperty('name'.toJS)?.toString(); - return header( - className: 'header', - children: [ - div( - className: 'header-content', - children: [ - h1('TaskFlow', className: 'logo'), - if (userName != null) - div( - className: 'user-info', - children: [ - span('Welcome, $userName', className: 'user-name'), - button( - text: 'Logout', - className: 'btn btn-ghost', - onClick: onLogout, - ), - ], - ) - else - span('', className: 'spacer'), - ], - ), - ], - ); -} diff --git a/examples/frontend/web/types.dart b/examples/frontend/web/types.dart deleted file mode 100644 index 0520f33..0000000 --- a/examples/frontend/web/types.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'dart:js_interop'; - -/// API configuration -const apiUrl = 'http://localhost:3000'; - -/// WebSocket URL -const wsUrl = 'ws://localhost:3001'; - -/// Auth state - immutable record -typedef AuthState = ({JSString? token, JSObject? user, String view}); - -/// Auth actions typeclass - Haskell style effect abstraction -typedef SetToken = void Function(JSString?); -typedef SetUser = void Function(JSObject?); -typedef SetView = void Function(String); - -/// Auth effects bundle -typedef AuthEffects = ({SetToken setToken, SetUser setUser, SetView setView}); - -/// Form state setter typeclass -typedef SetFormValue = void Function(T); - -/// Loading effect -typedef SetLoading = void Function(bool); - -/// Error effect -typedef SetError = void Function(String?); - -/// Form effects bundle -typedef FormEffects = ({SetLoading setLoading, SetError setError}); - -/// Task operations typeclass -typedef OnToggleTask = void Function(String id, bool completed); -typedef OnDeleteTask = void Function(String id); - -/// Task effects bundle -typedef TaskEffects = ({OnToggleTask onToggle, OnDeleteTask onDelete}); - -/// Generic async effect - represents any async operation -typedef AsyncEffect = Future Function(); - -/// Event handler effect -typedef OnClick = void Function(); - -/// Input change handler -typedef OnInputChange = void Function(JSAny event); - -/// Helper to wrap JSFunction setState calls -SetFormValue wrapSetState(JSFunction setState) => - (value) => setState.callAsFunction(null, switch (value) { - final String s => s.toJS, - final bool b => b.toJS, - final int i => i.toJS, - final double d => d.toJS, - null => null, - _ => throw StateError('Unsupported type: ${value.runtimeType}'), - }); - -/// Helper to wrap JSFunction setState for nullable JSAny -void Function(JSAny?) wrapSetStateJSAny(JSFunction setState) => - (value) => setState.callAsFunction(null, value); - -/// Clear state helper -void Function() wrapClearState(JSFunction setState) => - () => setState.callAsFunction(); diff --git a/examples/mobile/analysis_options.yaml b/examples/mobile/analysis_options.yaml index fb2690c..ea2c9e9 100644 --- a/examples/mobile/analysis_options.yaml +++ b/examples/mobile/analysis_options.yaml @@ -1,6 +1 @@ -include: package:lints/recommended.yaml - -linter: - rules: - avoid_print: true - prefer_single_quotes: true +include: package:lints/recommended.yaml \ No newline at end of file diff --git a/examples/mobile/coverage/lcov.info b/examples/mobile/coverage/lcov.info new file mode 100644 index 0000000..5b7761d --- /dev/null +++ b/examples/mobile/coverage/lcov.info @@ -0,0 +1,498 @@ +SF:/Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/app.dart +DA:15,1 +DA:17,1 +DA:18,1 +DA:19,1 +DA:20,1 +DA:22,1 +DA:23,1 +DA:24,1 +DA:26,1 +DA:27,1 +DA:28,1 +DA:32,1 +DA:33,1 +DA:34,1 +DA:35,1 +DA:38,1 +DA:43,1 +DA:45,1 +DA:46,1 +DA:78,2 +DA:85,2 +DA:86,2 +DA:87,2 +DA:88,2 +DA:94,0 +DA:95,2 +LF:26 +LH:25 +end_of_record +SF:/Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/screens/login_screen.dart +DA:13,1 +DA:17,2 +DA:18,2 +DA:19,2 +DA:20,2 +DA:21,2 +DA:28,2 +DA:29,1 +DA:30,1 +DA:31,1 +DA:32,1 +DA:33,1 +DA:34,1 +DA:35,1 +DA:36,1 +DA:37,1 +DA:39,1 +DA:41,1 +DA:43,2 +DA:44,2 +DA:45,2 +DA:46,2 +DA:47,2 +DA:48,2 +DA:50,1 +DA:51,1 +DA:52,1 +DA:54,2 +DA:55,2 +DA:56,2 +DA:57,2 +DA:58,2 +DA:59,2 +DA:62,2 +DA:63,2 +DA:64,2 +DA:68,2 +DA:70,2 +DA:71,2 +DA:72,2 +DA:75,2 +DA:78,2 +DA:82,2 +DA:84,2 +DA:85,2 +DA:87,2 +DA:90,2 +DA:91,2 +DA:92,2 +DA:93,2 +DA:94,2 +DA:95,2 +DA:96,2 +DA:103,2 +DA:105,2 +DA:112,2 +DA:113,2 +DA:114,2 +DA:116,2 +DA:117,2 +DA:118,1 +DA:119,2 +DA:122,2 +DA:123,2 +DA:124,2 +DA:125,2 +DA:126,0 +DA:129,2 +DA:130,2 +DA:131,2 +DA:132,2 +DA:134,1 +DA:137,1 +DA:139,2 +DA:140,2 +DA:142,2 +DA:143,1 +DA:144,2 +DA:145,1 +DA:146,1 +DA:147,2 +LF:81 +LH:80 +end_of_record +SF:/Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/screens/register_screen.dart +DA:13,1 +DA:17,1 +DA:18,1 +DA:19,1 +DA:20,1 +DA:21,1 +DA:22,1 +DA:30,1 +DA:31,1 +DA:32,1 +DA:33,1 +DA:34,1 +DA:35,1 +DA:36,1 +DA:37,1 +DA:38,1 +DA:39,1 +DA:41,1 +DA:43,1 +DA:44,1 +DA:45,1 +DA:46,1 +DA:47,1 +DA:48,1 +DA:50,1 +DA:51,1 +DA:52,1 +DA:54,1 +DA:55,1 +DA:56,1 +DA:57,1 +DA:58,1 +DA:59,1 +DA:62,1 +DA:63,1 +DA:64,1 +DA:68,1 +DA:70,1 +DA:71,1 +DA:72,1 +DA:75,1 +DA:77,1 +DA:81,1 +DA:83,1 +DA:84,1 +DA:85,1 +DA:88,1 +DA:91,1 +DA:95,1 +DA:97,1 +DA:98,1 +DA:100,1 +DA:103,1 +DA:104,1 +DA:105,1 +DA:106,1 +DA:107,1 +DA:108,2 +DA:109,1 +DA:116,1 +DA:118,1 +DA:126,1 +DA:127,1 +DA:128,1 +DA:130,1 +DA:131,1 +DA:132,1 +DA:133,1 +DA:134,1 +DA:135,1 +DA:136,1 +DA:137,1 +DA:138,1 +DA:139,1 +DA:140,1 +DA:141,2 +DA:143,1 +DA:144,1 +DA:145,1 +DA:146,1 +DA:147,1 +DA:148,1 +LF:82 +LH:82 +end_of_record +SF:/Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/screens/task_list_screen.dart +DA:16,1 +DA:17,1 +DA:18,0 +DA:19,1 +DA:22,1 +DA:23,1 +DA:24,1 +DA:25,1 +DA:29,1 +DA:35,2 +DA:36,2 +DA:37,2 +DA:38,2 +DA:39,2 +DA:40,2 +DA:48,2 +DA:49,2 +DA:50,1 +DA:51,1 +DA:52,1 +DA:57,2 +DA:58,2 +DA:59,1 +DA:60,1 +DA:61,2 +DA:62,1 +DA:63,1 +DA:64,1 +DA:65,1 +DA:67,1 +DA:69,1 +DA:71,2 +DA:72,1 +DA:76,2 +DA:77,1 +DA:78,1 +DA:79,1 +DA:80,1 +DA:82,2 +DA:83,1 +DA:84,1 +DA:86,2 +DA:87,1 +DA:88,1 +DA:90,2 +DA:91,1 +DA:94,1 +DA:95,1 +DA:96,1 +DA:97,1 +DA:98,1 +DA:100,2 +DA:101,2 +DA:102,2 +DA:103,2 +DA:105,2 +DA:106,2 +DA:112,2 +DA:113,1 +DA:114,1 +DA:115,1 +DA:117,2 +DA:119,2 +DA:122,2 +DA:124,2 +DA:128,2 +DA:129,2 +DA:130,2 +DA:131,2 +DA:135,2 +DA:138,2 +DA:142,2 +DA:143,2 +DA:144,2 +DA:145,2 +DA:146,2 +DA:147,2 +DA:148,2 +DA:149,2 +DA:150,2 +DA:152,2 +DA:159,1 +DA:168,1 +DA:169,1 +DA:170,1 +DA:180,1 +DA:181,2 +DA:183,1 +DA:191,0 +DA:192,0 +DA:193,0 +DA:194,0 +DA:198,0 +DA:199,0 +DA:201,0 +DA:203,0 +DA:204,0 +DA:206,0 +DA:208,0 +DA:209,0 +DA:214,1 +DA:215,1 +DA:216,1 +DA:217,1 +DA:221,1 +DA:222,1 +DA:223,1 +DA:224,1 +DA:225,1 +DA:226,1 +DA:227,1 +DA:228,1 +DA:229,1 +DA:230,1 +DA:231,1 +DA:234,1 +DA:235,1 +DA:236,1 +DA:237,1 +DA:238,1 +DA:239,1 +DA:240,1 +DA:241,1 +DA:242,1 +DA:246,1 +DA:247,2 +DA:248,1 +DA:249,1 +DA:252,1 +DA:254,2 +DA:261,2 +DA:262,2 +DA:263,1 +DA:264,2 +DA:267,2 +DA:268,2 +DA:270,0 +DA:272,2 +DA:273,2 +DA:274,2 +DA:276,2 +DA:277,1 +DA:278,2 +DA:279,1 +DA:280,1 +DA:281,2 +DA:283,2 +DA:284,2 +DA:285,2 +DA:287,1 +DA:288,1 +DA:289,0 +DA:290,0 +DA:293,2 +DA:294,2 +DA:296,1 +DA:304,1 +DA:305,1 +DA:306,1 +DA:309,1 +DA:310,1 +DA:311,1 +DA:312,1 +DA:313,1 +DA:314,1 +DA:315,1 +DA:316,1 +DA:317,1 +DA:318,1 +DA:319,1 +DA:320,2 +DA:322,1 +DA:323,1 +DA:324,1 +DA:325,1 +DA:327,1 +DA:334,1 +DA:335,1 +DA:336,1 +DA:337,1 +DA:338,1 +DA:339,2 +DA:340,1 +DA:341,1 +DA:342,1 +DA:343,2 +DA:345,1 +DA:346,1 +DA:347,1 +DA:348,1 +DA:350,1 +DA:358,1 +DA:359,1 +DA:360,1 +DA:363,1 +DA:364,1 +DA:365,1 +DA:366,1 +DA:369,1 +DA:370,1 +DA:372,0 +DA:374,1 +DA:375,1 +DA:376,1 +DA:377,2 +DA:379,1 +DA:380,1 +DA:381,1 +DA:382,1 +DA:385,1 +DA:390,1 +DA:391,1 +DA:392,1 +DA:393,1 +DA:394,1 +DA:395,2 +DA:396,1 +DA:397,1 +DA:398,1 +LF:219 +LH:202 +end_of_record +SF:/Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/websocket.dart +DA:12,1 +DA:15,1 +DA:18,1 +DA:21,1 +DA:32,1 +DA:37,1 +DA:43,1 +DA:44,2 +DA:45,1 +DA:46,2 +DA:47,2 +DA:48,1 +DA:49,1 +DA:50,1 +DA:51,1 +DA:53,1 +DA:54,1 +DA:56,0 +DA:59,1 +DA:61,2 +DA:62,2 +DA:63,1 +DA:64,2 +DA:65,2 +DA:67,2 +DA:70,1 +DA:71,1 +DA:72,1 +DA:73,1 +LF:29 +LH:28 +end_of_record +SF:/Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/types.dart +DA:5,2 +DA:6,2 +DA:10,1 +DA:11,1 +DA:12,1 +DA:49,2 +DA:50,2 +DA:51,1 +DA:52,2 +DA:55,2 +DA:56,2 +DA:57,1 +DA:58,2 +DA:61,2 +DA:62,2 +DA:63,1 +DA:64,2 +DA:67,2 +DA:69,2 +DA:70,2 +DA:74,2 +DA:75,2 +DA:79,2 +DA:81,2 +DA:82,2 +DA:84,2 +DA:85,2 +DA:88,2 +DA:89,2 +DA:100,1 +DA:101,1 +DA:102,1 +DA:103,1 +DA:106,1 +DA:107,1 +DA:108,1 +DA:109,1 +LF:37 +LH:37 +end_of_record diff --git a/examples/mobile/coverage/test/login_screen_test.dart.chrome.json b/examples/mobile/coverage/test/login_screen_test.dart.chrome.json new file mode 100644 index 0000000..ef60b04 --- /dev/null +++ b/examples/mobile/coverage/test/login_screen_test.dart.chrome.json @@ -0,0 +1 @@ +{"type":"CodeCoverage","coverage":[{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/ast.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fast.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/ast.dart","_kind":"library"},"hits":[38,0,41,0,44,0,47,0,50,0,64,0,67,0,70,0,71,0,74,0,77,0,83,0,97,0,100,0,103,0,104,0,106,0,108,0,109,0,112,0,113,0,116,0,122,0,136,0,139,0,142,0,143,0,145,0,147,0,148,0,151,0,152,0,155,0,161,0,179,0,182,0,185,0,187,0,188,0,189,0,190,0,193,0,194,0,195,0,196,0,197,0,200,0,201,0,206,0,207,0,208,0,209,0,210,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/functions.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fcollection-1.19.1%2Flib%2Fsrc%2Ffunctions.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/functions.dart","_kind":"library"},"hits":[32,0,34,0,37,0,38,0,39,0,40,0,41,0,42,0,56,0,57,0,58,0,59,0,61,0,62,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/queue_list.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fcollection-1.19.1%2Flib%2Fsrc%2Fqueue_list.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/queue_list.dart","_kind":"library"},"hits":[31,0,32,0,38,0,39,0,43,0,44,0,67,0,69,0,76,0,81,0,82,0,83,0,116,0,122,0,140,0,141,0,142,0,143,0,159,0,162,0,163,0,164,0,165,0,167,0,170,0,171,0,172,0,173,0,175,0,176,0,179,0,180,0,181,0,183,0,184,0,185,0,187,0,188,0,191,0,192,0,193,0,196,0,197,0,200,0,201,0,202,0,205,0,206,0,220,0,221,0,222,0,223,0,224,0,225,0,228,0,231,0,232,0,233,0,234,0,235,0,239,0,240,0,241,0,242,0,243,0,244,0,245,0,248,0,249,0,250,0,251,0,252,0,253,0,255,0,256,0,257,0,258,0,260,0,263,0,264,0,268,0,269,0,270,0,271,0,272,0,273,0,274,0,282,0,285,0,288,0,291,0,294,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/unmodifiable_wrappers.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fcollection-1.19.1%2Flib%2Fsrc%2Funmodifiable_wrappers.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/unmodifiable_wrappers.dart","_kind":"library"},"hits":[108,0,120,0,121,0,122,0,127,0,132,0]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react/lib/src/hooks.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fpackages%2Fdart_node_react%2Flib%2Fsrc%2Fhooks.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react/lib/src/hooks.dart","_kind":"library"},"hits":[38,1,75,0,79,0,80,0,81,0,82,0,84,0,85,0,86,0]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react/lib/src/state_hook.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fpackages%2Fdart_node_react%2Flib%2Fsrc%2Fstate_hook.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react/lib/src/state_hook.dart","_kind":"library"},"hits":[16,0,27,1,32,0,33,0,34,0,35,0,73,0,81,0,82,0,83,0,124,0,130,0,133,0,147,0,151,0,152,0,153,0,161,0,167,1,170,1,173,1,174,1,175,1,176,1,177,1,179,1,180,1,184,0,185,0,187,0,188,0,190,0,192,0,193,0,207,0,211,0,212,0,213,0,214,0]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react/lib/src/testing_library.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fpackages%2Fdart_node_react%2Flib%2Fsrc%2Ftesting_library.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react/lib/src/testing_library.dart","_kind":"library"},"hits":[64,0,65,0,66,0,68,0,69,0,72,0,73,0,126,0,131,0,134,1,149,1,150,1,151,0,152,1,173,1,179,0,185,0,200,1,453,0,468,0,469,0,470,0,471,0,486,0,490,0,491,0,496,0,504,0,518,0,519,0,522,0,524,0,525,0,526,0,540,0,541,0,542,0,543,0,545,0,547,0,548,0,551,0,552,0,553,0,554,0,556,0,558,0,559,0,583,0,584,0,585,0,588,0,589,0,592,0,593,0,594,0,595,0,598,0,599,0,600,0,603,0,604,0,607,0,608,0,609,0,610,0,613,0,614,0,615,0,618,0,619,0,622,0,623,0,624,0,625,0,710,0,714,0,719,0,749,1,750,1,751,1,753,1,754,1,755,1,757,1,758,1,760,1]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react_native/lib/src/components.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fpackages%2Fdart_node_react_native%2Flib%2Fsrc%2Fcomponents.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react_native/lib/src/components.dart","_kind":"library"},"hits":[84,0,91,0,92,0,94,0,95,0,96,0,98,0,101,0,107,0,108,0,111,0,112,0,115,0,123,0,124,0,125,0,126,0,128,0,130,0,131,0,132,0,133,0,136,0,143,0,144,0,145,0,147,0,148,0,150,0,176,0,177,0,183,0,209,1,211,1,212,1,215,1]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react_native/lib/src/core.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fpackages%2Fdart_node_react_native%2Flib%2Fsrc%2Fcore.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react_native/lib/src/core.dart","_kind":"library"},"hits":[20,1,22,1,24,1,25,1,29,0,30,0,31,0,33,1,71,1,78,1,80,1,81,1,84,1,85,1,86,0,88,1,91,0,94,0,101,1]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_core/lib/src/node.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fpackages%2Fdart_node_core%2Flib%2Fsrc%2Fnode.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_core/lib/src/node.dart","_kind":"library"},"hits":[6,0]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react/lib/src/react.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fpackages%2Fdart_node_react%2Flib%2Fsrc%2Freact.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react/lib/src/react.dart","_kind":"library"},"hits":[97,1,98,1,104,1,121,1,131,0,132,0,133,0,134,0,136,0,137,0,139,1,140,1,142,1,144,1,145,1,147,1,148,1,149,0,150,1,151,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/description.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Fdescription.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/description.dart","_kind":"library"},"hits":[15,0,17,0,20,0,24,0,29,0,45,0,46,0,47,0,49,0,51,0,52,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/equals_matcher.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Fequals_matcher.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/equals_matcher.dart","_kind":"library"},"hits":[29,0,32,0,35,0,36,0,39,0,45,0,47,0,48,0,50,0,54,0,55,0,56,0,59,0,60,0,65,0,71,0,75,0,76,0,78,0,79,0,81,0,87,0,88,0,90,0,91,0,93,0,95,0,97,0,99,0,100,0,101,0,103,0,106,0,113,1,115,0,122,0,123,0,124,0,125,0,127,0,128,0,131,0,135,0,136,0,138,0,139,0,143,0,144,0,145,0,149,0,152,0,154,0,156,0,163,0,164,0,166,0,167,0,168,0,169,0,171,0,174,0,175,0,176,0,181,0,182,0,183,0,184,0,186,0,189,0,191,0,193,1,200,1,201,0,202,0,203,0,205,0,206,0,208,0,210,0,213,0,214,1,219,1,221,1,223,0,226,0,227,0,232,0,233,0,241,0,242,0,243,0,246,0,250,0,251,0,254,0,258,0,259,0,260,0,263,0,266,0,267,0,268,0,271,0,272,0,273,0,278,0,279,0,280,0,283,0,284,0,285,0,290,0,291,0,292,0,293,0,294,0,297,0,300,0,306,0,307,0,310,0,314,0,316,1,319,1,320,1,321,1,322,0,323,0,324,1,327,0,328,0,331,0,337,0,338,0,339,0,343,0,344,0,345,0,347,0,349,0,356,0,358,0,361,0,364,0,365,0,387,0,394,0,395,0,396,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/expect/expect.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Fexpect%2Fexpect.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/expect/expect.dart","_kind":"library"},"hits":[57,0,65,0,73,0,94,1,102,1,103,0,104,0,105,0,107,0,113,0,119,1,174,1,175,0,176,1,177,1,179,0,180,0,182,0,183,1,187,0,191,0,198,0,199,0,200,0,201,0,202,0,203,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/invoker.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Finvoker.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/invoker.dart","_kind":"library"},"hits":[53,0,63,0,75,1,76,1,77,1,78,1,81,0,82,0,83,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,92,0,129,1,144,0,158,1,165,1,168,0,169,0,170,0,171,0,175,0,181,1,198,1,203,0,209,0,210,0,211,0,212,0,214,0,216,0,218,0,226,1,229,1,231,1,237,1,240,1,241,1,244,0,251,0,253,0,254,0,256,0,271,0,272,0,278,0,279,0,288,0,289,0,290,1,291,1,295,1,296,0,297,0,299,0,301,1,302,0,309,0,310,0,314,1,315,1,316,1,318,1,320,1,322,1,324,0,325,0,326,0,327,0,333,1,334,0,335,1,336,1,339,1,340,1,341,1,342,0,346,0,347,0,349,1,350,0,351,0,352,0,353,0,354,0,355,1,391,0,393,0,396,0,397,0,398,0,400,0,402,0,405,0,408,0,409,0,410,0,413,0,415,0,416,0,417,0,422,0,428,0,433,0,435,0,438,1,439,1,442,1,443,1,444,1,445,0,446,1,455,1,457,1,458,1,459,0,462,1,464,1,465,0,466,0,467,0,469,0,470,0,473,1,474,1,477,1,478,1,479,0,480,0,481,0,482,0,485,0,488,0,489,1,490,1,493,1,497,1,498,1,500,1,505,0,510,0,521,1,523,0,524,1,525,1,526,1,532,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/hooks.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fhooks.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/hooks.dart","_kind":"library"},"hits":[23,0,26,0,30,0,84,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/stack_trace_formatter.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fstack_trace_formatter.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/stack_trace_formatter.dart","_kind":"library"},"hits":[13,0,20,0,26,0,30,0,35,1,42,0,50,0,55,0,56,0,57,0,58,0,66,0,67,0,69,0,70,0,72,0,74,0,75,0,76,0,77,0,78,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/test_failure.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Ftest_failure.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/test_failure.dart","_kind":"library"},"hits":[9,0,12,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/expect/util/pretty_print.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Fexpect%2Futil%2Fpretty_print.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/expect/util/pretty_print.dart","_kind":"library"},"hits":[12,0,13,0,14,0,15,0,17,0,20,0,23,0,24,0,25,0,32,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/pretty_print.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Fpretty_print.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/pretty_print.dart","_kind":"library"},"hits":[18,0,19,0,26,0,27,0,28,0,29,0,33,0,34,0,35,0,37,0,39,0,42,0,43,0,44,0,49,0,51,0,52,0,53,0,57,0,58,0,59,0,60,0,63,0,64,0,65,0,68,0,69,0,74,0,76,0,77,0,78,0,82,0,83,0,84,0,85,0,87,0,89,0,90,0,91,0,93,0,94,0,98,0,104,0,105,0,111,0,113,0,116,0,118,0,119,0,121,0,125,0,126,0,127,0,128,0,130,0,131,0,138,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/util.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Futil.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/util.dart","_kind":"library"},"hits":[21,0,22,0,27,0,28,0,29,0,30,0,38,1,39,1,40,1,41,1,43,0,44,1,47,0,49,1,51,1,57,0,59,0,60,0,61,0,62,0,63,0,64,0,67,0,68,0,69,0,70,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/core_matchers.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Fcore_matchers.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/core_matchers.dart","_kind":"library"},"hits":[17,1,18,1,21,0,55,1,57,0,69,1,71,0,239,1,242,1,243,1,244,1,245,1,246,0,250,0,252,0,253,0,255,0,256,1,259,0,260,0,263,0,269,0,270,0,271,0,272,0,274,0,276,0,337,0,340,0,343,0,344,0]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/app.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fexamples%2Fmobile%2Flib%2Fapp.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/app.dart","_kind":"library"},"hits":[15,0,17,0,18,0,19,0,20,0,22,0,23,0,24,0,26,0,27,0,28,0,32,0,33,0,34,0,35,0,38,0,43,0,45,0,46,0,78,1,85,1,86,1,87,1,88,1,94,0,95,1]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/screens/login_screen.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fexamples%2Fmobile%2Flib%2Fscreens%2Flogin_screen.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/screens/login_screen.dart","_kind":"library"},"hits":[13,0,17,1,18,1,19,1,20,1,21,1,28,1,29,0,30,0,31,0,32,0,33,0,34,0,35,0,36,0,37,0,39,0,41,0,43,1,44,1,45,1,46,1,47,1,48,1,50,1,51,1,52,1,54,1,55,1,56,1,57,1,58,1,59,1,62,1,63,1,64,1,68,1,70,1,71,1,72,1,75,1,78,1,82,1,84,1,85,1,87,1,90,1,91,1,92,1,93,1,94,1,95,1,96,1,103,1,105,1,112,1,113,1,114,1,116,1,117,1,118,0,119,1,122,1,123,1,124,1,125,1,126,0,129,1,130,1,131,1,132,1,134,1,137,1,139,1,140,1,142,1,143,1,144,1,145,0,146,0,147,1]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/screens/register_screen.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fexamples%2Fmobile%2Flib%2Fscreens%2Fregister_screen.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/screens/register_screen.dart","_kind":"library"},"hits":[13,1,17,1,18,1,19,1,20,1,21,1,22,1,30,1,31,1,32,1,33,1,34,1,35,1,36,1,37,1,38,1,39,1,41,1,43,1,44,1,45,1,46,1,47,1,48,1,50,1,51,1,52,1,54,1,55,1,56,1,57,1,58,1,59,1,62,1,63,1,64,1,68,1,70,1,71,1,72,1,75,1,77,1,81,1,83,1,84,1,85,1,88,1,91,1,95,1,97,1,98,1,100,1,103,1,104,1,105,1,106,1,107,1,108,1,109,1,116,1,118,1,126,1,127,1,128,1,130,1,131,1,132,1,133,1,134,1,135,1,136,1,137,1,138,1,139,1,140,1,141,1,143,1,144,1,145,1,146,1,147,1,148,1]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/screens/task_list_screen.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fexamples%2Fmobile%2Flib%2Fscreens%2Ftask_list_screen.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/screens/task_list_screen.dart","_kind":"library"},"hits":[16,1,17,1,18,0,19,1,22,1,23,1,24,1,25,1,29,0,35,1,36,1,37,1,38,1,39,1,40,1,48,1,49,1,50,0,51,0,52,0,57,1,58,1,59,0,60,0,61,1,62,1,63,1,64,1,65,1,67,1,69,1,71,1,72,0,76,1,77,1,78,1,79,1,80,1,82,1,83,1,84,1,86,1,87,1,88,1,90,1,91,1,94,1,95,1,96,1,97,1,98,1,100,1,101,1,102,1,103,1,105,1,106,1,112,1,113,1,114,1,115,1,117,1,119,1,122,1,124,1,128,1,129,1,130,1,131,1,135,1,138,1,142,1,143,1,144,1,145,1,146,1,147,1,148,1,149,1,150,1,152,1,159,0,168,0,169,0,170,0,180,0,181,1,183,0,191,0,192,0,193,0,194,0,198,0,199,0,201,0,203,0,204,0,206,0,208,0,209,0,214,0,215,0,216,0,217,0,221,1,222,1,223,1,224,1,225,1,226,1,227,1,228,1,229,1,230,1,231,1,234,1,235,1,236,1,237,1,238,1,239,1,240,1,241,1,242,1,246,1,247,1,248,1,249,1,252,1,254,1,261,1,262,1,263,0,264,1,267,1,268,1,270,0,272,1,273,1,274,1,276,1,277,1,278,1,279,0,280,0,281,1,283,1,284,1,285,1,287,1,288,1,289,0,290,0,293,1,294,1,296,1,304,1,305,1,306,1,309,1,310,1,311,1,312,1,313,1,314,1,315,1,316,1,317,1,318,1,319,1,320,1,322,1,323,1,324,1,325,1,327,1,334,1,335,1,336,1,337,1,338,1,339,1,340,1,341,1,342,1,343,1,345,1,346,1,347,1,348,1,350,1,358,1,359,1,360,1,363,1,364,1,365,1,366,1,369,1,370,1,372,0,374,1,375,1,376,1,377,1,379,1,380,1,381,1,382,1,385,1,390,1,391,1,392,1,393,1,394,1,395,1,396,1,397,1,398,1]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/websocket.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fexamples%2Fmobile%2Flib%2Fwebsocket.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/websocket.dart","_kind":"library"},"hits":[12,1,15,1,18,1,21,1,32,0,37,0,43,0,44,1,45,1,46,1,47,1,48,1,49,1,50,1,51,1,53,1,54,1,56,0,59,1,61,1,62,1,63,1,64,1,65,1,67,1,70,1,71,1,72,1,73,1]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/types.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fexamples%2Fmobile%2Flib%2Ftypes.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/types.dart","_kind":"library"},"hits":[5,1,6,1,10,0,11,0,12,0,49,1,50,1,51,0,52,1,55,1,56,1,57,0,58,1,61,1,62,1,63,0,64,1,67,1,69,1,70,1,74,1,75,1,79,1,81,1,82,1,84,1,85,1,88,1,89,1,100,0,101,0,102,0,103,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/nadz-0.0.7-beta/lib/nadz.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fnadz-0.0.7-beta%2Flib%2Fnadz.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/nadz-0.0.7-beta/lib/nadz.dart","_kind":"library"},"hits":[15,1,21,0,27,1,33,0,110,1,114,1,115,1,116,1,117,1,130,0,131,0,132,0,133,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/context.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fsrc%2Fcontext.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/context.dart","_kind":"library"},"hits":[28,0,44,0,45,0,48,0,49,0,52,0,62,0,66,0,77,0,92,0,112,0,113,0,116,0,118,0,217,0,223,0,233,0,248,0,264,0,282,0,283,0,284,0,300,0,301,0,302,0,305,0,311,0,312,0,313,0,314,0,317,0,324,0,326,0,335,0,338,0,339,0,364,0,367,0,368,0,369,0,370,0,401,0,402,0,405,0,406,0,407,0,410,0,411,0,419,0,420,0,422,0,426,0,427,0,428,0,433,0,435,0,437,0,440,0,446,0,449,0,450,0,459,0,462,0,465,0,467,0,469,0,472,0,473,0,507,0,509,0,511,0,515,0,521,0,527,0,530,0,531,0,533,0,534,0,541,0,543,0,544,0,548,0,549,0,550,0,551,0,552,0,553,0,554,0,560,0,561,0,563,0,564,0,565,0,566,0,569,0,573,0,574,0,575,0,576,0,577,0,582,0,583,0,585,0,586,0,595,0,609,0,613,0,615,0,616,0,617,0,618,0,619,0,620,0,621,0,622,0,623,0,625,0,626,0,627,0,628,0,632,0,633,0,636,0,637,0,641,0,644,0,645,0,646,0,647,0,648,0,649,0,651,0,652,0,656,0,659,0,661,0,662,0,670,0,676,0,677,0,678,0,679,0,680,0,687,0,695,0,696,0,697,0,698,0,699,0,704,0,705,0,710,0,711,0,713,0,715,0,716,0,717,0,727,0,728,0,732,0,733,0,736,0,738,0,744,0,745,0,746,0,747,0,748,0,758,0,759,0,760,0,761,0,763,0,764,0,768,0,769,0,770,0,771,0,772,0,781,0,782,0,783,0,786,0,788,0,791,0,800,0,801,0,802,0,808,0,809,0,810,0,818,0,827,0,836,0,845,0,846,0,847,0,849,0,864,0,865,0,866,0,868,0,870,0,871,0,875,0,879,0,880,0,884,0,886,0,887,0,888,0,890,0,893,0,897,0,900,0,904,0,907,0,910,0,911,0,912,0,913,0,914,0,1040,0,1058,0,1060,0,1062,0,1064,0,1091,0,1092,0,1093,0,1094,0,1095,0,1096,0,1097,0,1098,0,1101,0,1102,0,1107,0,1108,0,1110,0,1116,0,1118,0,1120,0,1124,0,1125,0,1127,0,1130,0,1131,0,1139,0,1140,0,1142,0,1144,0,1171,0,1200,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/parsed_path.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fsrc%2Fparsed_path.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/parsed_path.dart","_kind":"library"},"hits":[25,0,39,0,41,0,43,0,44,0,45,0,48,0,49,0,51,0,53,0,54,0,55,0,57,0,60,0,61,0,62,0,63,0,64,0,69,0,70,0,71,0,74,0,75,0,77,0,89,0,90,0,92,0,93,0,94,0,95,0,97,0,98,0,100,0,103,0,104,0,105,0,107,0,109,0,110,0,113,0,116,0,122,0,127,0,131,0,132,0,133,0,134,0,135,0,139,0,141,0,143,0,144,0,147,0,149,0,150,0,151,0,152,0,154,0,156,0,157,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/path_exception.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fsrc%2Fpath_exception.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/path_exception.dart","_kind":"library"},"hits":[10,0,13,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/style.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fsrc%2Fstyle.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/style.dart","_kind":"library"},"hits":[14,0,19,0,27,0,33,0,36,0,41,0,42,0,43,0,44,0,45,0,51,0,84,0]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/shared/lib/http/http_client.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fexamples%2Fshared%2Flib%2Fhttp%2Fhttp_client.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/shared/lib/http/http_client.dart","_kind":"library"},"hits":[16,0,22,0,23,0,24,0,25,0,26,0,33,0,35,0,37,0,38,0,39,0,41,0,42,0,43,0,44,0,45,0,46,0,49,0,50,0,52,0,53,0,54,0,55,0,57,0,58,0,59,0,61,0,72,0,73,0,74,0,78,0,80,0,85,0,87,0,89,0,90,0,92,0,93,0,95,0,99,0,101,0,102,0,104,0,106,0,108,0,111,0,112,0,114,0,116,0,118,0,119,0,120,0,121,0,122,0,124,0,126,0,127,0,128,0,129,0,131,0,132,0,139,0,140,0,141,0,142,0,143,0,144,0,146,0,147,0,148,0,149,0,152,0,153,0,154,0,155,0,157,0,159,0,161,0,162,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_map_stack_trace-2.1.2/lib/source_map_stack_trace.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_map_stack_trace-2.1.2%2Flib%2Fsource_map_stack_trace.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_map_stack_trace-2.1.2/lib/source_map_stack_trace.dart","_kind":"library"},"hits":[21,0,23,0,24,0,25,0,26,0,27,0,32,0,33,0,34,0,37,0,40,0,45,0,49,0,51,0,52,0,53,0,54,0,55,0,56,0,57,0,60,0,61,0,65,0,66,0,67,0,68,0,74,0,75,0,76,0,79,0,80,0,82,0,84,0,87,0,90,0,92,0,94,0,96,0,99,0,103,0,104,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/chain.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstack_trace-1.12.1%2Flib%2Fsrc%2Fchain.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/chain.dart","_kind":"library"},"hits":[19,0,51,0,77,1,82,1,87,1,88,1,95,0,96,0,97,0,98,0,101,0,107,0,109,0,111,0,112,0,114,1,143,0,144,0,146,0,147,0,150,0,151,0,152,0,153,0,154,0,164,0,165,0,166,0,167,0,168,0,169,0,176,0,177,0,178,0,179,0,181,0,182,0,184,0,186,0,187,0,190,0,216,0,218,0,219,0,221,0,222,0,227,0,228,0,229,0,234,0,237,0,238,0,244,0,247,0,249,0,250,0,251,0,252,0,253,0,257,0,258,0,259,0,260,0,261,0,262,0,263,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_maps-0.10.13/lib/parser.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_maps-0.10.13%2Flib%2Fparser.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_maps-0.10.13/lib/parser.dart","_kind":"library"},"hits":[37,0,45,0,47,0,48,0,50,0,51,0,58,0,60,0,61,0,65,0,66,0,67,0,68,0,69,0,72,0,75,0,76,0,99,0,102,0,106,0,109,0,111,0,112,0,113,0,115,0,116,0,118,0,119,0,121,0,122,0,124,0,125,0,127,0,128,0,130,0,133,0,138,0,139,0,141,0,145,0,147,0,149,0,150,0,151,0,152,0,154,0,155,0,158,0,162,0,163,0,164,0,166,0,169,0,171,0,174,0,176,0,178,0,182,0,183,0,187,0,191,0,192,0,193,0,195,0,202,0,203,0,210,0,211,0,212,0,213,0,215,0,216,0,221,0,224,0,238,0,239,0,240,0,241,0,242,0,243,0,244,0,248,0,258,0,259,0,260,0,261,0,346,0,347,0,348,0,349,0,350,0,351,0,352,0,353,0,354,0,355,0,356,0,357,0,358,0,359,0,360,0,361,0,370,0,371,0,374,0,376,0,377,0,379,0,396,0,398,0,399,0,402,0,403,0,404,0,406,0,408,0,410,0,411,0,414,0,415,0,417,0,421,0,424,0,427,0,428,0,429,0,430,0,499,0,500,0,501,0,506,0,507,0,508,0,509,0,516,0,517,0,518,0,519,0,520,0,521,0,522,0,525,0,527,0,528,0,530,0,531,0,533,0,534,0,538,0,539,0,550,0,551,0,552,0,553,0,556,0,557,0,559,0,562,0,565,0,566,0,568,0,570,0,572,0,574,0,576,0,579,0,621,0,624,0,635,0,642,0,643,0,651,0,653,0,657,0,660,0,661,0,662,0,664,0,666,0,667,0,668,0,669,0,670,0,671,0,672,0,674,0,686,0,688,0,689,0,692,0,693,0,696,0,698,0,699,0,701,0,702,0,703,0,714,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_maps-0.10.13/lib/src/source_map_span.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_maps-0.10.13%2Flib%2Fsrc%2Fsource_map_span.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_maps-0.10.13/lib/src/source_map_span.dart","_kind":"library"},"hits":[17,0,18,0,24,0,27,0,28,0,29,0,30,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_maps-0.10.13/lib/src/vlq.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_maps-0.10.13%2Flib%2Fsrc%2Fvlq.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_maps-0.10.13/lib/src/vlq.dart","_kind":"library"},"hits":[28,0,29,0,30,0,31,0,33,0,34,0,36,0,37,0,66,0,67,0,70,0,71,0,72,0,73,0,74,0,75,0,77,0,79,0,80,0,92,0,96,0,97,0,100,0,101,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/file.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_span-1.10.1%2Flib%2Fsrc%2Ffile.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/file.dart","_kind":"library"},"hits":[33,0,47,0,50,0,69,0,70,0,81,0,82,0,83,0,84,0,85,0,86,0,88,0,89,0,91,0,93,0,98,0,100,0,101,0,107,0,108,0,109,0,111,0,112,0,115,0,116,0,118,0,120,0,121,0,122,0,128,0,129,0,133,0,136,0,137,0,138,0,142,0,143,0,144,0,145,0,148,0,149,0,154,0,156,0,157,0,158,0,159,0,162,0,166,0,167,0,173,0,174,0,175,0,177,0,178,0,182,0,190,0,191,0,192,0,195,0,196,0,201,0,202,0,204,0,205,0,206,0,207,0,208,0,213,0,214,0,215,0,216,0,219,0,220,0,226,0,243,0,246,0,249,0,251,0,252,0,253,0,255,0,256,0,258,0,311,0,314,0,317,0,320,0,323,0,326,0,327,0,328,0,339,0,342,0,353,0,356,0,357,0,359,0,360,0,361,0,363,0,364,0,365,0,366,0,368,0,371,0,372,0,374,0,375,0,376,0,398,0,399,0,401,0,404,0,405,0,406,0,407,0,410,0,417,0,418,0,419,0,420,0,424,0,425,0,426,0,432,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/highlighter.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_span-1.10.1%2Flib%2Fsrc%2Fhighlighter.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/highlighter.dart","_kind":"library"},"hits":[61,0,62,0,63,0,64,0,65,0,66,0,101,0,104,0,107,0,108,0,109,0,110,0,111,0,115,0,116,0,117,0,118,0,119,0,120,0,121,0,124,0,125,0,129,0,133,0,134,0,135,0,136,0,137,0,140,0,141,0,142,0,146,0,147,0,148,0,151,0,152,0,155,0,157,0,158,0,160,0,161,0,163,0,168,0,170,0,172,0,174,0,175,0,176,0,177,0,179,0,181,0,184,0,185,0,186,0,191,0,192,0,199,0,201,0,202,0,203,0,204,0,205,0,206,0,208,0,209,0,210,0,219,0,220,0,221,0,222,0,223,0,228,0,230,0,231,0,234,0,235,0,237,0,238,0,240,0,241,0,243,0,244,0,245,0,246,0,248,0,250,0,254,0,255,0,256,0,261,0,262,0,263,0,267,0,268,0,269,0,271,0,272,0,274,0,276,0,277,0,285,0,290,0,293,0,296,0,299,0,300,0,301,0,302,0,303,0,304,0,305,0,306,0,309,0,310,0,311,0,312,0,313,0,314,0,315,0,316,0,317,0,319,0,322,0,323,0,324,0,325,0,326,0,327,0,328,0,329,0,330,0,331,0,332,0,333,0,334,0,335,0,336,0,340,0,341,0,342,0,344,0,347,0,351,0,353,0,354,0,356,0,357,0,363,0,365,0,366,0,367,0,368,0,369,0,372,0,373,0,374,0,376,0,377,0,378,0,379,0,380,0,381,0,383,0,384,0,385,0,386,0,389,0,390,0,392,0,393,0,396,0,397,0,398,0,400,0,401,0,402,0,403,0,405,0,408,0,409,0,410,0,411,0,413,0,418,0,419,0,420,0,422,0,423,0,427,0,428,0,429,0,433,0,434,0,441,0,443,0,444,0,445,0,447,0,460,0,464,0,465,0,488,0,492,0,493,0,494,0,495,0,497,0,500,0,507,0,508,0,512,0,513,0,514,0,515,0,516,0,517,0,518,0,521,0,522,0,523,0,524,0,526,0,527,0,530,0,531,0,532,0,534,0,535,0,539,0,540,0,541,0,542,0,543,0,544,0,566,0,567,0,568,0,569,0,570,0,571,0,572,0,574,0,583,0,584,0,586,0,587,0,588,0,589,0,590,0,591,0,592,0,593,0,594,0,598,0,599,0,600,0,602,0,603,0,604,0,605,0,609,0,610,0,611,0,612,0,613,0,614,0,616,0,617,0,623,0,625,0,629,0,631,0,632,0,633,0,634,0,635,0,636,0,640,0,641,0,642,0,643,0,644,0,647,0,648,0,652,0,653,0,654,0,656,0,658,0,659,0,660,0,661,0,662,0,663,0,667,0,668,0,669,0,670,0,674,0,675,0,676,0,677,0,678,0,680,0,682,0,684,0,688,0,689,0,690,0,691,0,694,0,697,0,698,0,700,0,701,0,721,0,723,0,726,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/location.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_span-1.10.1%2Flib%2Fsrc%2Flocation.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/location.dart","_kind":"library"},"hits":[35,0,36,0,45,0,47,0,50,0,51,0,52,0,53,0,54,0,55,0,57,0,62,0,63,0,64,0,65,0,67,0,68,0,77,0,78,0,79,0,80,0,82,0,83,0,86,0,87,0,88,0,89,0,92,0,95,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/span_exception.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_span-1.10.1%2Flib%2Fsrc%2Fspan_exception.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/span_exception.dart","_kind":"library"},"hits":[11,0,18,0,31,0,33,0,34,0,46,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/span_with_context.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_span-1.10.1%2Flib%2Fsrc%2Fspan_with_context.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/span_with_context.dart","_kind":"library"},"hits":[13,0,24,0,27,0,28,0,31,0,32,0,33,0,35,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/stack_zone_specification.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstack_trace-1.12.1%2Flib%2Fsrc%2Fstack_zone_specification.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/stack_zone_specification.dart","_kind":"library"},"hits":[37,0,40,0,64,0,70,0,71,0,72,0,73,0,80,0,87,0,88,0,91,0,92,0,96,0,97,0,99,0,101,0,104,0,106,0,110,0,112,0,114,0,115,0,119,0,124,0,126,0,127,0,128,0,132,0,134,0,137,0,138,0,139,0,173,0,175,0,178,0,179,0,181,0,184,0,185,0,186,0,195,0,203,0,204,0,205,0,207,0,208,0,212,0,213,0,215,0,217,0,221,0,222,0,223,0,224,0,225,0,228,0,230,0,231,0,235,0,236,0,237,0,238,0,239,0,250,0,253,0,254,0,256,0,257,0,258,0,260,0,261,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/lazy_chain.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstack_trace-1.12.1%2Flib%2Fsrc%2Flazy_chain.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/lazy_chain.dart","_kind":"library"},"hits":[18,0,20,0,23,0,27,0,28,0,30,0,32,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/frame.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstack_trace-1.12.1%2Flib%2Fsrc%2Fframe.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/frame.dart","_kind":"library"},"hits":[13,0,20,0,21,0,29,0,50,0,58,0,59,0,63,0,64,0,71,0,115,0,116,0,135,0,136,0,142,0,146,0,148,0,175,0,182,0,183,0,184,0,185,0,189,0,190,0,191,0,192,0,195,0,196,0,197,0,198,0,199,0,216,0,219,0,220,0,223,0,224,0,228,0,229,0,231,0,232,0,233,0,235,0,237,0,239,0,240,0,241,0,244,0,247,0,248,0,249,0,250,0,251,0,253,0,254,0,257,0,258,0,261,0,262,0,263,0,264,0,265,0,268,0,269,0,272,0,273,0,275,0,276,0,278,0,279,0,280,0,283,0,287,0,288,0,289,0,296,0,300,0,301,0,320,0,321,0,322,0,323,0,324,0,325,0,326,0,330,0,331,0,334,0,335,0,336,0,337,0,338,0,342,0,345,0,347,0,348,0,352,0,357,0,359,0,360,0,363,0,364,0,365,0,366,0,367,0,369,0,370,0,374,0,375,0,376,0,377,0,380,0,381,0,395,0,396,0,397,0,398,0,405,0,406,0,409,0,413,0,414,0,415,0,416,0,419,0,422,0,426,0,427,0,428,0,429,0,430,0,431,0,432,0,438,0,439,0,440,0,446,0,447,0,448,0,449,0,450,0,452,0,454,0,457,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/unparsed_frame.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstack_trace-1.12.1%2Flib%2Fsrc%2Funparsed_frame.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/unparsed_frame.dart","_kind":"library"},"hits":[12,0,29,0,32,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/lazy_trace.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstack_trace-1.12.1%2Flib%2Fsrc%2Flazy_trace.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/lazy_trace.dart","_kind":"library"},"hits":[16,0,18,0,21,0,23,0,29,0,30,0,32,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/trace.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstack_trace-1.12.1%2Flib%2Fsrc%2Ftrace.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/trace.dart","_kind":"library"},"hits":[14,0,22,0,28,0,40,0,55,0,67,0,68,0,93,0,99,0,100,0,101,0,104,0,105,0,107,0,113,0,114,0,115,0,116,0,117,0,124,0,125,0,126,0,127,0,128,0,129,0,130,0,131,0,133,0,134,0,135,0,141,0,142,0,143,0,145,0,148,0,150,0,153,0,155,0,156,0,157,0,160,0,163,0,166,0,167,0,170,0,171,0,174,0,176,0,182,0,183,0,184,0,187,0,189,0,191,0,192,0,193,0,202,0,204,0,205,0,206,0,207,0,208,0,209,0,233,0,236,0,237,0,238,0,239,0,241,0,242,0,243,0,246,0,247,0,248,0,284,0,285,0,287,0,288,0,290,0,291,0,300,0,301,0,302,0,305,0,306,0,307,0,308,0,309,0,310,0,315,0,316,0,317,0,318,0,319,0,321,0,322,0,326,0,327,0,330,0,333,0,336,0,337,0,338,0,339,0,340,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/src/guarantee_channel.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstream_channel-2.1.4%2Flib%2Fsrc%2Fguarantee_channel.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/src/guarantee_channel.dart","_kind":"library"},"hits":[16,0,19,0,20,1,27,1,35,1,37,0,46,1,47,1,50,1,52,1,53,1,54,0,55,0,56,0,57,1,59,1,65,0,66,0,67,0,68,0,69,0,70,0,85,0,106,1,114,1,118,1,119,1,121,0,123,1,125,1,126,1,129,0,130,0,132,0,134,0,136,0,137,0,143,0,144,0,145,0,146,0,149,0,152,0,153,0,157,0,158,0,161,0,162,0,164,0,166,0,168,0,169,0,170,0,171,0,172,0,173,0,174,0,175,0,178,0,179,0,180,0,183,0,184,0,186,0,187,0,188,0,191,0,192,0,198,0,199,0,200,0,202,0,203,0,204,0,205,0,206,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/src/multi_channel.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstream_channel-2.1.4%2Flib%2Fsrc%2Fmulti_channel.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/src/multi_channel.dart","_kind":"library"},"hits":[63,0,94,0,96,0,99,0,103,0,107,0,111,0,135,0,138,0,139,0,140,0,141,0,143,1,144,1,148,1,150,1,154,0,155,0,156,0,158,1,159,1,165,0,167,1,168,0,169,0,170,0,173,1,174,1,176,1,180,1,181,1,186,0,187,0,188,0,193,1,194,0,195,0,198,1,199,1,202,0,203,1,204,1,205,0,207,1,208,1,211,1,212,1,213,1,214,1,215,1,216,1,220,0,221,0,222,0,223,0,225,0,229,0,230,0,231,0,234,0,235,0,236,0,237,0,241,0,242,0,244,0,245,0,270,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/src/stream_channel_controller.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstream_channel-2.1.4%2Flib%2Fsrc%2Fstream_channel_controller.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/src/stream_channel_controller.dart","_kind":"library"},"hits":[39,1,46,1,58,0,59,0,60,0,66,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/stream_channel.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstream_channel-2.1.4%2Flib%2Fstream_channel.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/stream_channel.dart","_kind":"library"},"hits":[88,0,146,0,154,0,155,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/exception.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstring_scanner-1.4.1%2Flib%2Fsrc%2Fexception.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/exception.dart","_kind":"library"},"hits":[19,0,20,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/span_scanner.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstring_scanner-1.4.1%2Flib%2Fsrc%2Fspan_scanner.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/span_scanner.dart","_kind":"library"},"hits":[45,0,46,0,62,0,63,0,89,0,91,0,92,0,106,0,107,0,108,0,109,0,112,0,113,0,114,0,117,0,118,0,124,0,125,0,126,0,141,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test-1.28.0/lib/src/bootstrap/browser.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest-1.28.0%2Flib%2Fsrc%2Fbootstrap%2Fbrowser.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test-1.28.0/lib/src/bootstrap/browser.dart","_kind":"library"},"hits":[12,0,19,0,20,0,21,0,22,0,23,0,25,0,26,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/plugin/remote_platform_helpers.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Frunner%2Fplugin%2Fremote_platform_helpers.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/plugin/remote_platform_helpers.dart","_kind":"library"},"hits":[32,0,46,0,51,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test-1.28.0/lib/src/runner/browser/dom.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest-1.28.0%2Flib%2Fsrc%2Frunner%2Fbrowser%2Fdom.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test-1.28.0/lib/src/runner/browser/dom.dart","_kind":"library"},"hits":[14,0,32,0,97,0,111,0,157,0,159,0,160,0,202,0,204,0,205,0,206,0,207,0,210,0,211,0,218,0,222,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test-1.28.0/lib/src/runner/browser/post_message_channel.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest-1.28.0%2Flib%2Fsrc%2Frunner%2Fbrowser%2Fpost_message_channel.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test-1.28.0/lib/src/runner/browser/post_message_channel.dart","_kind":"library"},"hits":[15,0,17,0,19,0,22,1,25,1,26,1,29,0,30,0,31,0,34,0,35,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/compiler.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fcompiler.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/compiler.dart","_kind":"library"},"hits":[6,0,44,0,45,0,46,0,53,0,57,0,59,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/declarer.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fdeclarer.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/declarer.dart","_kind":"library"},"hits":[58,0,61,0,64,0,80,0,94,0,100,0,103,0,117,1,154,0,166,0,167,0,173,0,175,0,177,0,189,0,194,0,195,0,198,0,210,0,213,0,217,0,223,0,225,0,226,0,227,0,231,1,232,1,234,1,236,1,238,1,244,1,245,1,246,0,250,1,251,1,252,1,253,1,254,1,257,1,259,1,260,0,269,0,272,0,284,0,287,0,291,0,297,0,299,0,300,0,301,0,303,0,312,0,313,0,314,0,316,0,319,0,320,0,321,0,322,0,323,0,325,0,326,0,328,0,331,0,335,0,336,0,364,0,370,0,371,0,373,0,375,0,376,0,378,0,379,0,383,0,386,0,387,0,389,0,390,0,392,0,393,0,394,0,395,0,396,0,398,0,403,0,404,0,406,0,418,0,422,0,428,1,429,1,431,1,432,1,435,0,436,0,454,0,457,0,460,0,462,0,463,0,464,0,465,0,466,0,467,0,470,0,472,0,478,0,480,0,481,0,482,0,484,0,485,0,492,0,495,0,496,0,499,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/group.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fgroup.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/group.dart","_kind":"library"},"hits":[21,0,36,0,61,0,69,0,71,0,72,0,73,0,75,0,76,0,77,0,78,0,79,0,82,0,83,0,84,0,85,0,86,0,87,0,88,0,91,0,92,0,93,0,94,0,96,0,132,0,133,0,134,0,137,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/live_test_controller.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Flive_test_controller.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/live_test_controller.dart","_kind":"library"},"hits":[44,0,51,0,59,0,67,0,75,0,83,1,101,0,107,0,114,0,115,0,119,0,121,0,122,0,123,0,130,1,131,1,132,1,134,1,135,1,136,1,139,0,140,0,141,0,145,0,147,0,150,1,151,1,152,0,154,0,159,1,161,1,162,1,163,1,169,1,172,0,173,0,175,0,176,0,178,0,179,0,181,0,184,0,185,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/metadata.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fmetadata.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/metadata.dart","_kind":"library"},"hits":[24,0,33,0,40,0,44,0,51,0,76,0,79,0,96,0,137,0,138,0,153,0,162,0,176,0,177,0,178,0,179,0,180,0,181,0,182,0,183,0,184,0,185,0,186,0,187,0,188,0,192,0,193,0,194,0,199,0,200,0,201,0,202,0,203,0,205,0,206,0,207,0,212,0,224,0,225,0,230,0,232,0,233,0,234,0,235,0,236,0,242,0,254,0,256,0,262,0,263,0,264,0,269,0,271,0,272,0,275,0,277,0,278,0,279,0,280,0,281,0,282,0,283,0,284,0,285,0,286,0,287,0,288,0,289,0,290,0,293,0,294,0,295,0,296,0,297,0,299,0,302,0,303,0,304,0,305,0,306,0,307,0,309,0,313,0,315,0,316,0,317,0,320,0,322,0,323,0,324,0,327,0,332,0,333,0,334,0,335,0,336,0,337,0,338,0,345,0,346,0,347,0,348,0,349,0,350,0,351,0,352,0,353,0,354,0,355,0,356,0,357,0,359,0,360,0,361,0,362,0,365,0,366,0,369,0,382,0,383,0,384,0,385,0,386,0,387,0,388,0,389,0,390,0,391,0,392,0,393,0,406,0,410,0,411,0,413,0,414,0,415,0,416,0,417,0,418,0,419,0,423,0,425,0,426,0,427,0,428,0,430,0,431,0,432,0,433,0,434,0,435,0,436,0,437,0,438,0,440,0,441,0,442,0,444,0,446,0,449,0,450,0,451,0,452,0,453,0,455,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/configuration/timeout.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fconfiguration%2Ftimeout.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/configuration/timeout.dart","_kind":"library"},"hits":[48,0,51,0,123,0,124,0,125,0,126,0,127,0,128,0,133,1,134,1,135,1,136,1,139,0,142,1,143,1,144,1,145,1,148,0,149,0,150,0,151,0,152,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/operating_system.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Foperating_system.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/operating_system.dart","_kind":"library"},"hits":[44,0,45,0,46,0,47,0,69,0,74,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/platform_selector.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fplatform_selector.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/platform_selector.dart","_kind":"library"},"hits":[14,0,22,0,23,0,24,0,47,0,48,0,49,0,52,0,54,0,60,0,61,0,68,0,73,0,74,0,76,0,77,0,78,0,79,0,80,0,81,0,84,0,87,0,88,0,89,0,90,0,91,0,92,0,93,0,95,0,96,0,97,0,98,0,99,0,100,0,101,0,102,0,103,0,104,0,108,0,109,0,110,0,111,0,114,0,117,0,118,0,121,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/remote_listener.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fremote_listener.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/remote_listener.dart","_kind":"library"},"hits":[46,0,54,0,60,0,64,0,65,0,66,0,67,0,71,0,72,0,73,0,74,0,75,0,76,0,77,0,78,0,79,0,82,0,83,0,84,0,85,0,88,0,89,0,90,0,93,0,96,0,97,0,98,0,100,0,101,0,102,0,103,0,106,0,107,0,108,0,109,0,111,0,113,0,114,0,115,0,116,0,118,0,119,0,121,0,122,0,125,0,127,0,128,0,129,0,133,0,136,0,138,0,139,0,144,0,145,0,146,0,147,0,150,0,151,0,152,0,153,0,155,0,159,0,161,0,162,0,163,0,164,0,165,0,167,0,169,0,170,0,174,0,176,0,177,0,178,0,183,0,184,0,185,0,188,0,194,0,196,0,198,0,204,0,206,0,211,0,213,0,220,0,225,0,226,0,228,0,229,0,231,0,233,0,235,0,238,0,239,0,241,0,242,0,243,0,244,0,245,0,247,0,253,0,258,0,260,0,261,0,262,0,263,0,264,0,265,0,267,0,269,0,271,0,272,0,274,0,276,0,278,0,281,0,283,0,286,0,287,0,288,0,289,0,290,0,292,0,293,0,295,0,296,0,298,0,300,0,301,0,303,0,304,0,305,0,306,0,307,0,311,0,313,0,314,0,315,0,317,0,318,0,320,0,322,0,323,0,324,0,325,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/suite_channel_manager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fsuite_channel_manager.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/suite_channel_manager.dart","_kind":"library"},"hits":[8,0,11,0,15,0,18,0,21,0,22,0,23,0,24,0,25,0,27,0,29,0,30,0,32,0,36,0,37,0,38,0,39,0,41,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/runtime.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fruntime.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/runtime.dart","_kind":"library"},"hits":[116,0,127,0,133,0,134,0,135,0,136,0,140,0,141,0,142,0,143,0,147,0,148,0,149,0,150,0,151,0,153,0,154,0,155,0,158,0,159,0,164,0,169,0,173,0,178,0,179,0,180,0,181,0,183,0,238,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/suite.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fsuite.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/suite.dart","_kind":"library"},"hits":[39,0,44,0,45,0,46,0,47,0,48,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/suite_platform.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fsuite_platform.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/suite_platform.dart","_kind":"library"},"hits":[34,0,40,0,41,0,42,0,44,0,45,0,46,0,49,0,54,0,55,0,56,0,58,0,59,0,61,0,62,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/scaffolding/utils.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fscaffolding%2Futils.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/scaffolding/utils.dart","_kind":"library"},"hits":[16,0,17,0,19,0,20,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/engine.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Frunner%2Fengine.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/engine.dart","_kind":"library"},"hits":[70,0,92,0,93,0,100,0,101,0,102,0,103,0,104,0,105,0,107,0,113,0,122,0,123,0,130,0,160,0,161,0,165,0,170,0,174,0,178,0,182,0,186,0,187,0,192,0,198,0,221,0,231,0,232,0,233,0,234,0,235,0,236,0,237,0,239,0,240,0,276,0,277,0,278,0,280,0,282,0,283,0,284,0,285,0,286,0,288,0,289,0,297,0,300,0,302,0,303,0,311,0,313,0,314,0,315,0,316,0,317,0,318,0,319,0,324,0,325,0,327,0,328,0,336,0,341,0,343,0,344,0,345,0,346,0,347,0,351,0,352,0,355,0,357,0,364,0,365,0,367,0,368,0,369,0,370,0,372,0,373,0,383,0,384,0,388,0,389,0,392,0,394,0,400,0,405,0,406,0,408,0,409,0,410,0,411,0,412,0,413,0,414,0,415,0,416,0,417,0,419,0,423,0,427,0,429,0,433,0,435,0,440,0,441,0,447,0,452,0,454,0,455,0,456,0,457,0,458,0,461,0,462,0,463,0,465,0,466,0,467,0,469,0,470,0,475,0,476,0,477,0,478,0,482,0,483,0,554,0,555,0,557,0,558,0,559,0,560,0,561,0,616,0,617,0,618,0,619,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/stream_group.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fstream_group.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/stream_group.dart","_kind":"library"},"hits":[32,0,42,0,85,0,120,0,122,0,138,0,139,0,140,0,143,0,144,0,145,0,149,0,151,0,154,0,155,0,169,0,170,0,174,0,176,0,186,0,187,0,189,0,193,0,195,0,197,0,202,0,203,0,206,0,227,0,228,0,230,0,231,0,232,0,234,0,235,0,237,0,239,0,243,0,251,0,252,0,257,0,258,0,260,0,265,0,266,0,267,0,268,0,269,0,274,0,275,0,276,0,277,0,278,0,279,0,288,0,289,0,291,0,292,0,294,0,295,0,335,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/union_set_controller.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fcollection-1.19.1%2Flib%2Fsrc%2Funion_set_controller.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/union_set_controller.dart","_kind":"library"},"hits":[36,0,47,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/pool-1.5.2/lib/pool.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpool-1.5.2%2Flib%2Fpool.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/pool-1.5.2/lib/pool.dart","_kind":"library"},"hits":[24,0,30,0,37,0,66,0,73,0,81,0,98,0,99,0,100,0,103,0,104,0,105,0,106,0,107,0,110,0,111,0,112,0,114,0,242,0,244,0,245,0,247,0,249,0,250,0,251,0,254,0,255,0,257,0,258,0,259,0,278,0,279,0,281,0,282,0,283,0,285,0,287,0,290,0,291,0,293,0,300,0,301,0,302,0,303,0,304,0,305,0,308,0,309,0,310,0,313,0,314,0,316,0,321,0,349,0,374,0,375,0,377,0,378,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/async_memoizer.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fasync_memoizer.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/async_memoizer.dart","_kind":"library"},"hits":[29,0,33,0,37,0,42,0,43,0,44,0,45,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/future_group.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Ffuture_group.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/future_group.dart","_kind":"library"},"hits":[21,0,35,0,69,0,70,0,75,0,76,0,79,0,80,0,83,0,85,0,89,0,91,0,92,0,93,0,94,0,95,0,96,0,101,0,102,0,103,0,104,0,105,0,106,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/union_set.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fcollection-1.19.1%2Flib%2Fsrc%2Funion_set.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/union_set.dart","_kind":"library"},"hits":[32,0,46,0,50,0,51,0,52,0,55,0,61,0,62,0,63,0,64,0,67,0,79,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/live_suite_controller.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Frunner%2Flive_suite_controller.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/live_suite_controller.dart","_kind":"library"},"hits":[21,0,34,0,37,0,40,0,43,0,48,0,61,0,82,0,85,0,88,0,99,0,108,0,109,0,110,0,113,0,114,0,116,0,118,0,119,0,120,0,122,0,123,0,124,0,125,0,126,0,127,0,128,0,130,0,132,0,134,0,136,0,137,0,142,0,143,0,147,0,148,0,149,0,151,0,153,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/suite.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Frunner%2Fsuite.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/suite.dart","_kind":"library"},"hits":[47,0,71,0,80,0,145,0,270,0,288,0,289,0,290,0,291,0,292,0,294,0,322,0,323,0,327,0,330,0,331,0,333,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/scaffolding.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Fscaffolding.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/scaffolding.dart","_kind":"library"},"hits":[38,0,40,0,41,0,47,0,49,0,50,0,54,0,55,0,56,0,57,0,60,0,71,0,72,0,73,0,75,0,76,0,78,0,80,0,81,0,144,0,157,0,173,0,175,0,231,0,244,0,260,0,262,0,275,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/util/stack_trace_mapper.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Futil%2Fstack_trace_mapper.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/util/stack_trace_mapper.dart","_kind":"library"},"hits":[28,0,40,0,41,0,44,0,45,0,62,0,64,0,65,0,68,0,69,0,70,0,72,0,74,0,87,0,91,0,92,0]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/test/login_screen_test.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fexamples%2Fmobile%2Ftest%2Flogin_screen_test.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/test/login_screen_test.dart","_kind":"library"},"hits":[20,0,21,0,25,0,26,0,27,0,29,0,30,0,31,0,33,0,34,0,36,1,37,1,38,0,40,0,42,0,45,0,48,0,50,0,53,0,55,0,56,0,59,0,61,0,63,1,64,1,66,1,67,1,68,1,71,1,74,1,75,1,78,1,80,1,81,1,83,1,84,1,86,1,87,1,89,1,92,1,93,1,96,1,98,1,100,1,101,1,103,1,104,1,105,1,107,1,111,1,114,1,115,1,118,1,120,1,122,1,123,1,125,1,126,1,127,1,130,1,133,1,134,1,137,1,139,1,141,1,142,1,144,1,145,1,147,1,150,1,152,1,153,1,156,1,158,1,160,1,161,1,162,0,166,0,167,1,168,1,169,1,171,1,173,1,176,1,179,1,181,1,184,1,185,1,186,1,189,1,191,1,193,1,194,1,196,1,197,1,198,1,201,1,204,1,206,1,209,1,210,1,211,1,214,1,216,1,218,1,219,1,221,1,222,1,224,1,227,1,229,1,232,1,233,1,234,1,237,1,239,1,241,1,242,1,244,1,245,1,246,1,248,1,252,1,255,1,258,1,259,1,260,1,263,1,266,1,268,1,269,1,271,1,272,1,273,1,276,1,279,1,282,1,283,1,284,1,287,1,290,1,292,1,293,1,294,0,298,0,299,1,302,1,305,1,306,1,309,1,311,1,312,1,313,1,315,1,316,1,317,1,319,1,321,1,323,1,324,1,325,1,330,1,332,1,333,1,335,1,336,1,338,1,339,1,340,1,342,1,344,1,347,1,349,1,351,1,352,1,354,1,355,1,356,1,358,1,360,1,363,1,365,1,367,1,368,1,370,1,371,1,372,1,374,1,376,1,379,1,381,1,383,1,384,1,386,1,387,1,388,1,390,1,392,1,395,1,397,1,399,1,400,1,402,1,403,1,404,1,410,1,411,1,413,1,416,1,418,1,420,0,421,1,423,1,426,1,427,1,430,1,432,1,433,1,435,1,437,1,438,1,440,1,441,1,442,1,444,1,446,1,449,1,451,1,456,1,457,1,458,1,459,1,463,1,464,1,466,1,467,1,469,1,470,1,471,1,473,1,475,1,477,1,478,1,481,1,484,1,486,1,492,1,494,1,495,1,500,1,502,1,503,1,505,1,506,1,507,1,509,1,511,1,513,1,514,1,517,1,520,1,522,1,525,1,527,1,528,1,532,1,534,1,535,1,537,1,538,1,539,1,545,1,546,1,548,1,551,1,552,1,554,1,555,1,559,1,560,1,561,1,563,0,564,1,566,1,569,1,570,1,573,1,575,1,578,1,580,1,581,1,585,1,586,1,588,1,589,1,591,1,592,1,593,1,595,1,597,1,599,1,600,1,603,1,606,1,608,1,612,1,613,1,614,1,615,1,620,1,621,1,623,1,624,1,626,1,627,1,628,1,630,1,632,1,634,1,635,1,638,1,641,1,643,1,646,1,647,1,648,1,649,1,654,1,656,1,657,1,659,1,660,1,661,1,667,1,668,1,670,1,673,1,674,1,676,1,677,1,681,1,682,1,683,1,685,0,686,1,688,1,691,1,692,1,695,1,697,1,700,1,701,1,702,1,703,1,707,1,708,1,710,1,711,1,713,1,714,1,715,1,717,1,719,1,722,1,724,1,728,1,730,1,731,1,736,1,737,1,739,1,740,1,742,1,743,1,744,1,746,1,748,1,751,1,755,1,757,1,758,1,762,1,766,1,768,1,769,1,774,1,775,1,777,1,778,1,780,1,781,1,782,1,784,1,786,1,787,1,789,1,793,1,797,1,799,1,800,1,804,1,808,1,812,1,814,1,815,1,819,1,821,1,823,1,824,1,826,1,827,1,828,1,830,1,832,1,835,1,839,1,841,1,842,1,846,1,850,1,852,1,853,1,858,1,859,1,861,1,862,1,864,1,865,1,866,1,868,1,870,1,871,1,874,1,877,1,879,1,880,1,884,1,887,1,890,1,892,1,893,1,897,1,899,1,900,1,902,1,903,1,904,1,910,1,911,1,913,1,916,1,917,1,918,1,922,1,923,1,925,0,926,1,928,1,931,1,932,1,935,1,937,1,940,1,942,1,943,1,947,1,950,1,953,1,955,1,956,1,960,1,961,1,963,1,964,1,965,0,969,0,970,1,971,1,972,1,974,1,976,1,979,1,982,1,983,1,986,1,988,1,991,1,996,1,998,1,999,1,1001,1,1002,1,1003,1,1005,1,1007,1,1009,1,1010,1,1015,1,1018,1,1019,1,1022,1,1024,1,1027,1,1032,1,1034,1,1035,1,1037,1,1038,1,1039,1,1041,1,1043,1,1045,1,1046,1,1051,1,1054,1,1055,1,1058,1,1060,1,1063,1,1068,1,1070,1,1071,1,1073,1,1074,1,1075,1,1077,1,1079,1,1081,1,1082,1,1087,1,1090,1,1091,1,1094,1,1096,1,1099,1,1105,1,1106,1,1108,1,1109,1,1111,1,1112,1,1113,1,1115,1,1117,1,1119,1,1120,1,1125,1,1128,1,1129,1,1132,1,1134,1,1137,1,1140,1,1141,1,1143,1,1144,1,1145,0,1149,0,1150,1,1151,1,1152,1,1153,1,1155,1,1157,1,1158,1,1159,1,1162,1,1167,1,1170,1,1176,1,1182,1,1184,1,1185,1,1187,1,1189,1,1197,1,1199,1,1201,1,1202,1,1204,1,1206,1,1210,1,1218,1,1219,0,1220,0]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/test/test_helpers.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fexamples%2Fmobile%2Ftest%2Ftest_helpers.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/test/test_helpers.dart","_kind":"library"},"hits":[25,0,26,0,27,0,28,0,29,0,30,0,32,0,33,1,34,1,36,1,38,1,39,1,40,1,41,0,42,1,43,0,44,1,45,1,46,1,47,1,48,1,49,1,53,1,54,1,55,0,56,0,58,0,59,1,60,1,61,1,62,1,63,1,64,1,65,1,66,0,67,0,68,1,69,0,70,0,101,0,102,1,104,1,105,1,106,1,107,1,111,1,112,1,113,1,114,1,117,1,118,0,120,1,121,1,122,1,123,1,124,1,125,1,128,1,129,1,136,0,137,0,138,0,139,1,140,0,142,0,143,0,144,0,147,1,148,1,149,1,151,1,154,1,155,1,160,0,161,0,162,0,167,1,173,1,174,1,175,1,177,0,178,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/path.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fpath.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/path.dart","_kind":"library"},"hits":[45,0,51,0,58,0,63,0,68,0,73,0,74,0,75,0,76,0,77,0,78,0,83,0,86,0,87,0,89,0,92,0,93,0,94,0,96,0,97,0,136,0,437,0,459,0,481,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/utils.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fsrc%2Futils.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/utils.dart","_kind":"library"},"hits":[9,0,10,0,11,0,19,0,32,0,33,0,34,0,35,0,37,0,38,0,39,0,45,0,46,0,47,0,48,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_maps-0.10.13/lib/src/utils.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_maps-0.10.13%2Flib%2Fsrc%2Futils.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_maps-0.10.13/lib/src/utils.dart","_kind":"library"},"hits":[13,0,14,0,15,0,16,0,19,0,20,0,21,0,22,0,25,0,28,0,29,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/utils.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_span-1.10.1%2Flib%2Fsrc%2Futils.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/utils.dart","_kind":"library"},"hits":[22,0,23,0,24,0,25,0,26,0,27,0,30,0,31,0,34,0,37,0,38,0,39,0,40,0,41,0,44,0,45,0,46,0,47,0,50,0,51,0,54,0,55,0,56,0,57,0,59,0,60,0,66,0,69,0,71,0,72,0,73,0,74,0,79,0,80,0,84,0,85,0,87,0,89,0,90,0,93,0,94,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/utils.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstring_scanner-1.4.1%2Flib%2Fsrc%2Futils.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/utils.dart","_kind":"library"},"hits":[8,0,10,0,15,0,16,0,17,0,18,0,27,0,28,0,31,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/remote_exception.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fremote_exception.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/remote_exception.dart","_kind":"library"},"hits":[32,0,33,0,34,0,35,0,37,0,38,0,39,0,46,0,48,0,50,0,51,0,53,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/util/pretty_print.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Futil%2Fpretty_print.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/util/pretty_print.dart","_kind":"library"},"hits":[9,0,10,0,12,0,13,0,20,0,21,0,23,0,24,0,25,0,26,0,31,0,32,0,37,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/util/pretty_print.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Futil%2Fpretty_print.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/util/pretty_print.dart","_kind":"library"},"hits":[19,0,20,0,21,0,25,0,28,0,29,0,30,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/delegate/stream_subscription.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fdelegate%2Fstream_subscription.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/delegate/stream_subscription.dart","_kind":"library"},"hits":[34,0,35,0,36,0,39,0,40,0,41,0,44,0,45,0,46,0,49,0,50,0,51,0,54,0,55,0,56,0,59,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/null_stream_sink.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fnull_stream_sink.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/null_stream_sink.dart","_kind":"library"},"hits":[46,0,59,0,60,0,61,0,69,0,70,0,72,0,73,0,74,0,75,0,76,0,77,0,81,0,82,0,83,0,84,0,86,0,89,0,90,0,91,0,92,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/result/error.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fresult%2Ferror.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/result/error.dart","_kind":"library"},"hits":[27,0,31,0,32,0,33,0,36,0,37,0,38,0,63,0,67,0,68,0,69,0,70,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/result/value.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fresult%2Fvalue.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/result/value.dart","_kind":"library"},"hits":[24,0,27,0,28,0,29,0,32,0,33,0,34,0,40,0,43,0,44,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/stream_completer.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fstream_completer.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/stream_completer.dart","_kind":"library"},"hits":[25,0,76,0,77,0,78,0,80,0,81,0,88,0,89,0,106,0,120,0,122,0,123,0,124,0,127,0,128,0,130,0,131,0,132,0,135,0,137,0,142,0,152,0,153,0,155,0,160,0,161,0,162,0,163,0,164,0,165,0,173,0,174,0,175,0,179,0,180,0,181,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/stream_queue.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fstream_queue.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/stream_queue.dart","_kind":"library"},"hits":[110,0,115,0,177,0,204,0,205,0,417,0,418,0,419,0,420,0,422,0,426,0,429,0,437,0,438,0,439,0,440,0,442,0,444,0,445,0,446,0,448,0,450,0,454,0,455,0,456,0,464,0,472,0,473,0,474,0,475,0,476,0,477,0,478,0,479,0,480,0,482,0,484,0,486,0,503,0,504,0,505,0,506,0,507,0,512,0,513,0,521,0,528,0,529,0,530,0,531,0,534,0,689,0,690,0,691,0,692,0,694,0,695,0,696,0,698,0,699,0,885,0,886,0,887,0,888,0,890,0,895,0,896,0,897,0,899,0,900,0,901,0,902,0,904,0,905,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/subscription_stream.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fsubscription_stream.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/subscription_stream.dart","_kind":"library"},"hits":[32,0,35,0,37,0,38,0,39,0,43,0,45,0,46,0,47,0,50,0,55,0,56,0,57,0,58,0,59,0,60,0,71,0,74,0,76,0,78,0,79,0,80,0,81,0,82,0,84,0,85,0,86,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/stream_sink_completer.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fstream_sink_completer.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/stream_sink_completer.dart","_kind":"library"},"hits":[20,0,31,0,62,0,63,0,65,0,81,0,101,0,104,0,105,0,106,0,107,0,108,0,110,0,111,0,132,0,133,0,135,0,136,0,139,0,140,0,141,0,143,0,145,0,146,0,149,0,150,0,151,0,159,0,160,0,161,0,165,0,168,0,170,0,171,0,176,0,177,0,179,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/all.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fall.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/all.dart","_kind":"library"},"hits":[17,0,20,0,26,0,29,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/evaluator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fevaluator.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/evaluator.dart","_kind":"library"},"hits":[13,0,16,0,19,0,22,0,23,0,26,0,27,0,30,0,31,0,32,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/impl.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fimpl.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/impl.dart","_kind":"library"},"hits":[28,0,29,0,31,0,37,0,38,0,41,0,42,0,43,0,44,0,45,0,47,0,59,0,60,0,61,0,64,0,67,0,68,0,71,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/intersection_selector.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fintersection_selector.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/intersection_selector.dart","_kind":"library"},"hits":[19,0,22,0,23,0,26,0,27,0,33,0,34,0,35,0,36,0,39,0,42,0,43,0,44,0,45,0,48,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/validator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fvalidator.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/validator.dart","_kind":"library"},"hits":[16,0,19,0,20,0,21,0,22,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/none.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fnone.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/none.dart","_kind":"library"},"hits":[15,0,18,0,24,0,27,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/parser.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fparser.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/parser.dart","_kind":"library"},"hits":[21,0,26,0,27,0,29,0,30,0,31,0,34,0,35,0,42,0,43,0,44,0,46,0,47,0,48,0,51,0,52,0,53,0,59,0,60,0,61,0,62,0,63,0,69,0,70,0,71,0,72,0,73,0,81,0,82,0,83,0,85,0,86,0,89,0,90,0,91,0,92,0,94,0,97,0,100,0,102,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/scanner.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fscanner.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/scanner.dart","_kind":"library"},"hits":[12,0,18,0,24,0,37,0,43,0,49,0,50,0,51,0,52,0,53,0,54,0,61,0,62,0,63,0,64,0,65,0,68,0,69,0,71,0,72,0,73,0,76,0,77,0,78,0,79,0,80,0,81,0,82,0,83,0,84,0,86,0,92,0,93,0,95,0,96,0,103,0,104,0,112,0,113,0,118,0,119,0,124,0,125,0,126,0,129,0,134,0,135,0,137,0,140,0,142,0,143,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/string_scanner.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstring_scanner-1.4.1%2Flib%2Fsrc%2Fstring_scanner.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/string_scanner.dart","_kind":"library"},"hits":[23,0,38,0,41,0,42,0,43,0,52,0,73,0,74,0,84,0,87,0,88,0,89,0,183,0,184,0,185,0,186,0,187,0,189,0,190,0,199,0,200,0,202,0,204,0,208,0,212,0,213,0,226,0,227,0,228,0,229,0,230,0,269,0,270,0,271,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/token.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Ftoken.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/token.dart","_kind":"library"},"hits":[19,0,32,0,35,0,73,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/visitor.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fvisitor.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/visitor.dart","_kind":"library"},"hits":[27,0,28,0,29,0,32,0,33,0,34,0,35,0,38,0,39,0,40,0,41,0,44,0,45,0,46,0,47,0,48,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/empty_unmodifiable_set.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fcollection-1.19.1%2Flib%2Fsrc%2Fempty_unmodifiable_set.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/empty_unmodifiable_set.dart","_kind":"library"},"hits":[17,0,19,0,23,0,39,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/wrappers.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fcollection-1.19.1%2Flib%2Fsrc%2Fwrappers.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/wrappers.dart","_kind":"library"},"hits":[26,0,29,0,32,0,38,0,55,0,61,0,67,0,74,0,77,0,94,0,100,0,106,0,109,0,112,0,118,0,320,0,340,0,341,0,342,0,392,0,395,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/feature_matcher.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Ffeature_matcher.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/feature_matcher.dart","_kind":"library"},"hits":[15,0,16,0,21,0,27,0,28,0,36,0,37,0,39,0,44,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/interfaces.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Finterfaces.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/interfaces.dart","_kind":"library"},"hits":[57,0,62,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/operator_matchers.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Foperator_matchers.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/operator_matchers.dart","_kind":"library"},"hits":[9,1,17,1,18,1,21,0,22,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/order_matchers.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Forder_matchers.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/order_matchers.dart","_kind":"library"},"hits":[121,1,122,1,123,0,124,1,125,0,126,1,127,1,129,0,131,1,134,0,136,0,137,0,138,0,139,0,143,0,146,0,153,0,154,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/type_matcher.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Ftype_matcher.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/type_matcher.dart","_kind":"library"},"hits":[94,0,95,0,96,0,97,0,100,0,103,0,109,0,110,0,111,0,114,0,123,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/internal_style.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fsrc%2Finternal_style.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/internal_style.dart","_kind":"library"},"hits":[46,0,47,0,48,0,49,0,50,0,63,0,64,0,65,0,69,0,70,0,71,0,79,0,85,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/style/posix.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fsrc%2Fstyle%2Fposix.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/style/posix.dart","_kind":"library"},"hits":[10,0,20,0,22,0,24,0,29,0,32,0,35,0,36,0,39,0,40,0,41,0,42,0,45,0,51,0,52,0,53,0,55,0,56,0,59,0,60,0,61,0,65,0,66,0,69,0,72,0,73,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/style/url.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fsrc%2Fstyle%2Furl.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/style/url.dart","_kind":"library"},"hits":[10,0,20,0,22,0,24,0,26,0,29,0,32,0,35,0,36,0,39,0,43,0,44,0,47,0,48,0,49,0,51,0,52,0,53,0,54,0,55,0,59,0,60,0,61,0,65,0,66,0,67,0,71,0,72,0,75,0,76,0,82,0,85,0,87,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/style/windows.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fsrc%2Fstyle%2Fwindows.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/style/windows.dart","_kind":"library"},"hits":[15,0,25,0,27,0,29,0,31,0,34,0,37,0,38,0,41,0,42,0,43,0,44,0,47,0,48,0,49,0,50,0,51,0,54,0,55,0,56,0,57,0,59,0,63,0,65,0,67,0,69,0,70,0,71,0,74,0,84,0,85,0,86,0,89,0,90,0,94,0,95,0,99,0,101,0,102,0,105,0,106,0,107,0,112,0,113,0,115,0,118,0,121,0,122,0,130,0,131,0,136,0,137,0,139,0,141,0,144,0,145,0,148,0,149,0,153,0,156,0,157,0,158,0,161,0,162,0,163,0,164,0,165,0,166,0,169,0,170,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/restartable_timer.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Frestartable_timer.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/restartable_timer.dart","_kind":"library"},"hits":[39,0,40,0,45,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/top_level.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fterm_glyph-1.2.2%2Flib%2Fsrc%2Fgenerated%2Ftop_level.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/top_level.dart","_kind":"library"},"hits":[61,0,67,0,73,0,85,0,97,0,127,0,133,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/term_glyph.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fterm_glyph-1.2.2%2Flib%2Fterm_glyph.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/term_glyph.dart","_kind":"library"},"hits":[21,0,31,0,37,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/location_mixin.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_span-1.10.1%2Flib%2Fsrc%2Flocation_mixin.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/location_mixin.dart","_kind":"library"},"hits":[20,0,24,0,25,0,26,0,27,0,29,0,30,0,36,0,37,0,38,0,39,0,41,0,42,0,45,0,46,0,47,0,48,0,51,0,54,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/span.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_span-1.10.1%2Flib%2Fsrc%2Fspan.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/span.dart","_kind":"library"},"hits":[103,0,104,0,105,0,106,0,107,0,108,0,109,0,110,0,113,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/span_mixin.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_span-1.10.1%2Flib%2Fsrc%2Fspan_mixin.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/span_mixin.dart","_kind":"library"},"hits":[20,0,23,0,26,0,27,0,28,0,29,0,53,0,54,0,55,0,56,0,57,0,59,0,66,0,67,0,70,0,71,0,72,0,73,0,76,0,77,0,80,0,83,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/ascii_glyph_set.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fterm_glyph-1.2.2%2Flib%2Fsrc%2Fgenerated%2Fascii_glyph_set.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/ascii_glyph_set.dart","_kind":"library"},"hits":[16,0,32,0,34,0,36,0,40,0,44,0,54,0,56,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/unicode_glyph_set.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fterm_glyph-1.2.2%2Flib%2Fsrc%2Fgenerated%2Funicode_glyph_set.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/unicode_glyph_set.dart","_kind":"library"},"hits":[16,0,32,0,34,0,36,0,40,0,44,0,54,0,56,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/closed_exception.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fclosed_exception.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/closed_exception.dart","_kind":"library"},"hits":[11,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/live_test.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Flive_test.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/live_test.dart","_kind":"library"},"hits":[61,1,118,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/state.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fstate.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/state.dart","_kind":"library"},"hits":[26,0,31,1,32,1,35,0,38,0,39,0,40,0,41,0,42,0,43,0,47,0,66,0,70,0,97,0,108,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/message.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fmessage.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/message.dart","_kind":"library"},"hits":[17,0,18,0,40,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/src/stream_channel_completer.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstream_channel-2.1.4%2Flib%2Fsrc%2Fstream_channel_completer.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/src/stream_channel_completer.dart","_kind":"library"},"hits":[23,0,42,0,53,0,54,0,56,0,57,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/test.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Ftest.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/test.dart","_kind":"library"},"hits":[24,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/util/iterable_set.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Frunner%2Futil%2Fiterable_set.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/util/iterable_set.dart","_kind":"library"},"hits":[23,0,26,0,29,0,32,0,43,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/runner_suite.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Frunner%2Frunner_suite.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/runner_suite.dart","_kind":"library"},"hits":[33,0,63,0,87,0,101,0,122,0,194,0,195,0,198,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/reporter/expanded.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Frunner%2Freporter%2Fexpanded.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/reporter/expanded.dart","_kind":"library"},"hits":[89,0,129,0,133,0,159,0,160,0,161,0,163,0,164,0,167,0,168,0,169,0,173,0,178,0,179,0,180,0,192,0,193,0,194,0,198,0,199,0,200,0,201,0,202,0,203,0,204,0,206,0,209,0,210,0,214,0,215,0,217,0,220,0,221,0,223,0,226,0,228,0,231,0,232,0,233,0,234,0,244,0,250,0,251,0,255,0,257,0,258,0,259,0,260,0,261,0,262,0,263,0,266,0,268,0,270,0,273,0,274,0,282,0,289,0,291,0,292,0,293,0,294,0,296,0,297,0,300,0,301,0,302,0,303,0,304,0,306,0,307,0,308,0,313,0,315,0,316,0,319,0,321,0,326,0,328,0,337,0,338,0,342,0,343,0,350,0,351,0,367,0,368,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/util/os.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Futil%2Fos.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/util/os.dart","_kind":"library"},"hits":[25,0,26,0,27,0,28,0,29,0,30,0,32,0,33,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/util/print_sink.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Futil%2Fprint_sink.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/util/print_sink.dart","_kind":"library"},"hits":[27,0,28,0,29,0,30,0,33,0,34,0,35,0,38,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/utils.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstack_trace-1.12.1%2Flib%2Fsrc%2Futils.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/utils.dart","_kind":"library"},"hits":[11,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/util/identifier_regex.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Futil%2Fidentifier_regex.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/util/identifier_regex.dart","_kind":"library"},"hits":[9,0]}]} \ No newline at end of file diff --git a/examples/mobile/coverage/test/types_test.dart.chrome.json b/examples/mobile/coverage/test/types_test.dart.chrome.json new file mode 100644 index 0000000..bcf3aeb --- /dev/null +++ b/examples/mobile/coverage/test/types_test.dart.chrome.json @@ -0,0 +1 @@ +{"type":"CodeCoverage","coverage":[{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/ast.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fast.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/ast.dart","_kind":"library"},"hits":[38,1,41,1,44,1,47,0,50,0,64,0,67,0,70,0,71,0,74,0,77,0,83,1,97,0,100,0,103,0,104,0,106,0,108,0,109,0,112,0,113,0,116,0,122,1,136,0,139,0,142,0,143,0,145,0,147,0,148,0,151,0,152,0,155,0,161,1,179,0,182,0,185,0,187,0,188,0,189,0,190,0,193,0,194,0,195,0,196,0,197,0,200,0,201,0,206,0,207,0,208,0,209,0,210,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/functions.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fcollection-1.19.1%2Flib%2Fsrc%2Ffunctions.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/functions.dart","_kind":"library"},"hits":[32,1,34,1,37,1,38,0,39,0,40,0,41,1,42,1,56,0,57,0,58,0,59,0,61,0,62,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/queue_list.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fcollection-1.19.1%2Flib%2Fsrc%2Fqueue_list.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/queue_list.dart","_kind":"library"},"hits":[31,0,32,1,38,1,39,1,43,1,44,1,67,1,69,1,76,1,81,0,82,1,83,0,116,0,122,0,140,1,141,1,142,1,143,1,159,1,162,0,163,0,164,0,165,0,167,0,170,0,171,0,172,0,173,0,175,0,176,0,179,0,180,0,181,0,183,0,184,0,185,0,187,0,188,0,191,0,192,0,193,0,196,0,197,0,200,0,201,0,202,0,205,0,206,0,220,0,221,0,222,0,223,0,224,0,225,0,228,0,231,1,232,1,233,1,234,1,235,1,239,0,240,0,241,0,242,0,243,0,244,0,245,0,248,0,249,0,250,0,251,0,252,0,253,0,255,0,256,0,257,0,258,0,260,0,263,0,264,0,268,0,269,0,270,0,271,0,272,0,273,0,274,0,282,0,285,1,288,0,291,0,294,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/unmodifiable_wrappers.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fcollection-1.19.1%2Flib%2Fsrc%2Funmodifiable_wrappers.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/unmodifiable_wrappers.dart","_kind":"library"},"hits":[108,1,120,0,121,0,122,0,127,0,132,0]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react/lib/src/hooks.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fpackages%2Fdart_node_react%2Flib%2Fsrc%2Fhooks.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react/lib/src/hooks.dart","_kind":"library"},"hits":[38,1,75,1,79,1,80,1,81,1,82,1,84,1,85,1,86,1]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react/lib/src/state_hook.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fpackages%2Fdart_node_react%2Flib%2Fsrc%2Fstate_hook.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react/lib/src/state_hook.dart","_kind":"library"},"hits":[16,1,27,1,32,1,33,1,34,1,35,1,73,1,81,1,82,1,83,1,124,1,130,1,133,1,147,1,151,1,152,1,153,1,161,1,167,1,170,1,173,0,174,0,175,0,176,0,177,0,179,0,180,0,184,1,185,1,187,1,188,1,190,0,192,1,193,1,207,1,211,1,212,1,213,1,214,1]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react/lib/src/testing_library.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fpackages%2Fdart_node_react%2Flib%2Fsrc%2Ftesting_library.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react/lib/src/testing_library.dart","_kind":"library"},"hits":[64,1,65,1,66,1,68,1,69,1,72,1,73,1,126,1,131,1,134,1,149,1,150,1,151,0,152,1,173,1,179,1,185,1,200,1,453,1,468,1,469,1,470,1,471,1,486,1,490,1,491,1,496,1,504,1,518,1,519,1,522,1,524,1,525,1,526,1,540,1,541,1,542,1,543,1,545,1,547,1,548,1,551,1,552,1,553,1,554,1,556,1,558,1,559,1,583,1,584,1,585,1,588,1,589,1,592,1,593,1,594,1,595,1,598,1,599,1,600,1,603,1,604,1,607,1,608,1,609,1,610,1,613,1,614,1,615,1,618,1,619,1,622,1,623,1,624,1,625,1,710,1,714,1,719,1,749,1,750,1,751,1,753,1,754,1,755,1,757,1,758,1,760,1]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react_native/lib/src/components.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fpackages%2Fdart_node_react_native%2Flib%2Fsrc%2Fcomponents.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react_native/lib/src/components.dart","_kind":"library"},"hits":[84,1,91,1,92,1,94,1,95,1,96,1,98,1,101,1,107,1,108,1,111,1,112,1,115,1,123,1,124,1,125,1,126,1,128,1,130,1,131,1,132,1,133,1,136,1,143,1,144,1,145,1,147,1,148,1,150,1,176,1,177,1,183,1,209,1,211,1,212,1,215,1]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react_native/lib/src/core.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fpackages%2Fdart_node_react_native%2Flib%2Fsrc%2Fcore.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react_native/lib/src/core.dart","_kind":"library"},"hits":[20,1,22,1,24,1,25,1,29,0,30,0,31,0,33,1,71,1,78,1,80,1,81,1,84,1,85,1,86,0,88,1,91,1,94,1,101,1]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_core/lib/src/node.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fpackages%2Fdart_node_core%2Flib%2Fsrc%2Fnode.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_core/lib/src/node.dart","_kind":"library"},"hits":[6,0]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react/lib/src/react.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fpackages%2Fdart_node_react%2Flib%2Fsrc%2Freact.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/packages/dart_node_react/lib/src/react.dart","_kind":"library"},"hits":[97,1,98,1,104,1,121,1,131,1,132,1,133,1,134,1,136,1,137,1,139,1,140,1,142,1,144,1,145,1,147,1,148,1,149,0,150,1,151,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/description.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Fdescription.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/description.dart","_kind":"library"},"hits":[15,0,17,0,20,0,24,0,29,0,45,0,46,0,47,0,49,0,51,0,52,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/equals_matcher.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Fequals_matcher.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/equals_matcher.dart","_kind":"library"},"hits":[29,1,32,1,35,0,36,0,39,0,45,0,47,0,48,0,50,0,54,0,55,0,56,0,59,0,60,0,65,0,71,0,75,0,76,0,78,0,79,0,81,0,87,0,88,0,90,0,91,0,93,0,95,0,97,0,99,0,100,0,101,0,103,0,106,0,113,0,115,0,122,0,123,0,124,0,125,0,127,0,128,0,131,0,135,0,136,0,138,0,139,0,143,0,144,0,145,0,149,0,152,0,154,0,156,0,163,0,164,0,166,0,167,0,168,0,169,0,171,0,174,0,175,0,176,0,181,0,182,0,183,0,184,0,186,0,189,0,191,0,193,0,200,0,201,0,202,0,203,0,205,0,206,0,208,0,210,0,213,0,214,0,219,0,221,0,223,0,226,0,227,0,232,0,233,0,241,0,242,0,243,0,246,0,250,0,251,0,254,0,258,0,259,0,260,0,263,0,266,0,267,0,268,0,271,0,272,0,273,0,278,0,279,0,280,0,283,0,284,0,285,0,290,0,291,0,292,0,293,0,294,0,297,0,300,0,306,0,307,0,310,1,314,0,316,0,319,0,320,0,321,0,322,0,323,0,324,0,327,0,328,0,331,0,337,0,338,0,339,0,343,0,344,0,345,0,347,0,349,0,356,0,358,0,361,0,364,0,365,0,387,0,394,0,395,0,396,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/expect/expect.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Fexpect%2Fexpect.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/expect/expect.dart","_kind":"library"},"hits":[57,1,65,1,73,1,94,1,102,1,103,0,104,0,105,0,107,0,113,0,119,1,174,1,175,0,176,1,177,1,179,0,180,0,182,0,183,1,187,0,191,0,198,0,199,0,200,0,201,0,202,0,203,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/invoker.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Finvoker.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/invoker.dart","_kind":"library"},"hits":[53,1,63,1,75,1,76,1,77,1,78,1,81,1,82,1,83,1,84,1,85,1,86,1,87,1,88,1,89,1,90,1,92,1,129,1,144,0,158,1,165,1,168,0,169,0,170,0,171,0,175,0,181,1,198,1,203,1,209,1,210,0,211,0,212,0,214,0,216,0,218,1,226,1,229,1,231,1,237,1,240,1,241,1,244,0,251,0,253,0,254,0,256,0,271,0,272,0,278,0,279,0,288,1,289,1,290,1,291,1,295,1,296,0,297,0,299,0,301,1,302,1,309,1,310,1,314,1,315,1,316,1,318,1,320,1,322,1,324,1,325,1,326,1,327,1,333,1,334,0,335,1,336,1,339,1,340,1,341,1,342,0,346,0,347,0,349,1,350,0,351,0,352,0,353,0,354,0,355,1,391,0,393,0,396,0,397,0,398,0,400,0,402,0,405,0,408,0,409,0,410,0,413,0,415,0,416,0,417,0,422,0,428,0,433,0,435,0,438,1,439,1,442,1,443,1,444,1,445,1,446,1,455,1,457,1,458,1,459,1,462,1,464,1,465,0,466,0,467,0,469,0,470,0,473,1,474,1,477,1,478,1,479,1,480,1,481,1,482,1,485,1,488,1,489,1,490,1,493,1,497,1,498,1,500,1,505,0,510,1,521,1,523,0,524,1,525,1,526,1,532,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/hooks.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fhooks.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/hooks.dart","_kind":"library"},"hits":[23,0,26,0,30,1,84,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/stack_trace_formatter.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fstack_trace_formatter.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/stack_trace_formatter.dart","_kind":"library"},"hits":[13,1,20,1,26,1,30,1,35,1,42,1,50,1,55,1,56,1,57,1,58,1,66,0,67,0,69,0,70,0,72,0,74,0,75,0,76,0,77,0,78,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/test_failure.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Ftest_failure.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/test_failure.dart","_kind":"library"},"hits":[9,0,12,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/expect/util/pretty_print.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Fexpect%2Futil%2Fpretty_print.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/expect/util/pretty_print.dart","_kind":"library"},"hits":[12,0,13,0,14,0,15,0,17,0,20,0,23,0,24,0,25,0,32,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/pretty_print.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Fpretty_print.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/pretty_print.dart","_kind":"library"},"hits":[18,0,19,1,26,0,27,0,28,0,29,0,33,0,34,0,35,1,37,0,39,0,42,0,43,0,44,0,49,0,51,0,52,0,53,0,57,0,58,0,59,0,60,0,63,0,64,0,65,0,68,0,69,0,74,0,76,0,77,0,78,0,82,0,83,0,84,0,85,0,87,0,89,0,90,0,91,0,93,0,94,0,98,0,104,0,105,0,111,0,113,0,116,0,118,0,119,0,121,0,125,0,126,0,127,0,128,0,130,0,131,0,138,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/util.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Futil.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/util.dart","_kind":"library"},"hits":[21,1,22,0,27,0,28,0,29,0,30,0,38,1,39,1,40,1,41,0,43,0,44,0,47,1,49,0,51,1,57,0,59,0,60,0,61,0,62,0,63,0,64,0,67,0,68,0,69,0,70,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/core_matchers.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Fcore_matchers.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/core_matchers.dart","_kind":"library"},"hits":[17,0,18,1,21,0,55,0,57,0,69,1,71,0,77,1,79,0,239,1,242,1,243,1,244,1,245,1,246,0,250,0,252,0,253,0,255,0,256,1,259,0,260,0,263,0,269,0,270,0,271,0,272,0,274,0,276,0,337,0,340,1,343,0,344,0]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/app.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fexamples%2Fmobile%2Flib%2Fapp.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/app.dart","_kind":"library"},"hits":[15,1,17,1,18,1,19,1,20,1,22,1,23,1,24,1,26,1,27,1,28,1,32,1,33,1,34,1,35,1,38,1,43,1,45,1,46,1,78,1,85,1,86,1,87,1,88,1,94,0,95,1]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/screens/login_screen.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fexamples%2Fmobile%2Flib%2Fscreens%2Flogin_screen.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/screens/login_screen.dart","_kind":"library"},"hits":[13,1,17,1,18,1,19,1,20,1,21,1,28,1,29,1,30,1,31,1,32,1,33,1,34,1,35,1,36,1,37,1,39,1,41,1,43,1,44,1,45,1,46,1,47,1,48,1,50,0,51,0,52,0,54,1,55,1,56,1,57,1,58,1,59,1,62,1,63,1,64,1,68,1,70,1,71,1,72,1,75,1,78,1,82,1,84,1,85,1,87,1,90,1,91,1,92,1,93,1,94,1,95,1,96,1,103,1,105,1,112,1,113,1,114,1,116,1,117,1,118,1,119,1,122,1,123,1,124,1,125,1,126,0,129,1,130,1,131,1,132,1,134,0,137,0,139,1,140,1,142,1,143,0,144,1,145,1,146,1,147,1]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/screens/register_screen.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fexamples%2Fmobile%2Flib%2Fscreens%2Fregister_screen.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/screens/register_screen.dart","_kind":"library"},"hits":[13,0,17,0,18,0,19,0,20,0,21,0,22,0,30,0,31,0,32,0,33,0,34,0,35,0,36,0,37,0,38,0,39,0,41,0,43,0,44,0,45,0,46,0,47,0,48,0,50,0,51,0,52,0,54,0,55,0,56,0,57,0,58,0,59,0,62,0,63,0,64,0,68,0,70,0,71,0,72,0,75,0,77,0,81,0,83,0,84,0,85,0,88,0,91,0,95,0,97,0,98,0,100,0,103,0,104,0,105,0,106,0,107,0,108,1,109,0,116,0,118,0,126,0,127,0,128,0,130,0,131,0,132,0,133,0,134,0,135,0,136,0,137,0,138,0,139,0,140,0,141,1,143,0,144,0,145,0,146,0,147,0,148,0]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/screens/task_list_screen.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fexamples%2Fmobile%2Flib%2Fscreens%2Ftask_list_screen.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/screens/task_list_screen.dart","_kind":"library"},"hits":[16,0,17,0,18,0,19,0,22,0,23,0,24,0,25,0,29,1,35,1,36,1,37,1,38,1,39,1,40,1,48,1,49,1,50,1,51,1,52,1,57,1,58,1,59,1,60,1,61,1,62,0,63,0,64,0,65,0,67,0,69,0,71,1,72,1,76,1,77,0,78,0,79,0,80,0,82,1,83,0,84,0,86,1,87,0,88,0,90,1,91,0,94,0,95,0,96,0,97,0,98,0,100,1,101,1,102,1,103,1,105,1,106,1,112,1,113,0,114,0,115,0,117,1,119,1,122,1,124,1,128,1,129,1,130,1,131,1,135,1,138,1,142,1,143,1,144,1,145,1,146,1,147,1,148,1,149,1,150,1,152,1,159,1,168,1,169,1,170,1,180,1,181,1,183,1,191,0,192,0,193,0,194,0,198,0,199,0,201,0,203,0,204,0,206,0,208,0,209,0,214,1,215,1,216,1,217,1,221,0,222,0,223,0,224,0,225,0,226,0,227,0,228,0,229,0,230,0,231,0,234,0,235,0,236,0,237,0,238,0,239,0,240,0,241,0,242,0,246,0,247,1,248,0,249,0,252,0,254,1,261,1,262,1,263,1,264,1,267,1,268,1,270,0,272,1,273,1,274,1,276,1,277,0,278,1,279,1,280,1,281,1,283,1,284,1,285,1,287,0,288,0,289,0,290,0,293,1,294,1,296,0,304,0,305,0,306,0,309,0,310,0,311,0,312,0,313,0,314,0,315,0,316,0,317,0,318,0,319,0,320,1,322,0,323,0,324,0,325,0,327,0,334,0,335,0,336,0,337,0,338,0,339,1,340,0,341,0,342,0,343,1,345,0,346,0,347,0,348,0,350,0,358,0,359,0,360,0,363,0,364,0,365,0,366,0,369,0,370,0,372,0,374,0,375,0,376,0,377,1,379,0,380,0,381,0,382,0,385,0,390,0,391,0,392,0,393,0,394,0,395,1,396,0,397,0,398,0]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/websocket.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fexamples%2Fmobile%2Flib%2Fwebsocket.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/websocket.dart","_kind":"library"},"hits":[12,0,15,0,18,0,21,0,32,1,37,1,43,1,44,1,45,0,46,1,47,1,48,0,49,0,50,0,51,0,53,0,54,0,56,0,59,0,61,1,62,1,63,0,64,1,65,1,67,1,70,0,71,0,72,0,73,0]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/types.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fexamples%2Fmobile%2Flib%2Ftypes.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/lib/types.dart","_kind":"library"},"hits":[5,1,6,1,10,1,11,1,12,1,49,1,50,1,51,1,52,1,55,1,56,1,57,1,58,1,61,1,62,1,63,1,64,1,67,1,69,1,70,1,74,1,75,1,79,1,81,1,82,1,84,1,85,1,88,1,89,1,100,1,101,1,102,1,103,1,106,1,107,1,108,1,109,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/nadz-0.0.7-beta/lib/nadz.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fnadz-0.0.7-beta%2Flib%2Fnadz.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/nadz-0.0.7-beta/lib/nadz.dart","_kind":"library"},"hits":[15,1,21,1,27,1,33,0,110,1,114,1,115,1,116,0,117,1,130,0,131,1,132,0,133,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/context.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fsrc%2Fcontext.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/context.dart","_kind":"library"},"hits":[28,0,44,0,45,0,48,0,49,0,52,0,62,0,66,0,77,0,92,0,112,0,113,0,116,0,118,0,217,0,223,0,233,0,248,0,264,0,282,0,283,0,284,0,300,0,301,0,302,0,305,1,311,0,312,0,313,0,314,0,317,0,324,0,326,0,335,0,338,0,339,0,364,0,367,1,368,0,369,0,370,0,401,0,402,0,405,0,406,0,407,0,410,0,411,0,419,0,420,0,422,0,426,0,427,0,428,0,433,0,435,0,437,0,440,0,446,0,449,0,450,0,459,0,462,0,465,0,467,0,469,0,472,0,473,0,507,0,509,0,511,0,515,0,521,0,527,0,530,0,531,0,533,0,534,0,541,0,543,0,544,0,548,0,549,0,550,0,551,0,552,0,553,0,554,0,560,0,561,0,563,0,564,0,565,0,566,0,569,0,573,0,574,0,575,0,576,0,577,0,582,0,583,0,585,0,586,0,595,0,609,0,613,0,615,0,616,0,617,0,618,0,619,0,620,0,621,0,622,0,623,0,625,0,626,0,627,0,628,0,632,0,633,0,636,0,637,0,641,0,644,0,645,0,646,0,647,0,648,0,649,0,651,0,652,0,656,0,659,0,661,0,662,0,670,0,676,0,677,0,678,0,679,0,680,0,687,0,695,0,696,0,697,0,698,0,699,0,704,0,705,0,710,0,711,0,713,0,715,0,716,0,717,0,727,0,728,0,732,0,733,0,736,0,738,0,744,0,745,0,746,0,747,0,748,0,758,0,759,0,760,0,761,0,763,0,764,0,768,0,769,0,770,0,771,0,772,0,781,0,782,0,783,0,786,0,788,0,791,0,800,0,801,0,802,0,808,0,809,0,810,0,818,0,827,0,836,0,845,0,846,0,847,0,849,0,864,0,865,0,866,0,868,0,870,0,871,0,875,0,879,0,880,0,884,0,886,0,887,0,888,0,890,0,893,0,897,0,900,0,904,0,907,0,910,0,911,0,912,0,913,0,914,0,1040,0,1058,0,1060,0,1062,0,1064,0,1091,0,1092,0,1093,0,1094,0,1095,0,1096,0,1097,0,1098,0,1101,0,1102,0,1107,0,1108,0,1110,0,1116,0,1118,0,1120,0,1124,0,1125,0,1127,0,1130,0,1131,0,1139,0,1140,0,1142,0,1144,0,1171,0,1200,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/parsed_path.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fsrc%2Fparsed_path.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/parsed_path.dart","_kind":"library"},"hits":[25,0,39,0,41,0,43,0,44,0,45,0,48,0,49,0,51,0,53,0,54,0,55,0,57,0,60,0,61,0,62,0,63,0,64,0,69,0,70,0,71,0,74,0,75,0,77,0,89,0,90,0,92,0,93,0,94,0,95,0,97,0,98,0,100,0,103,0,104,0,105,0,107,0,109,0,110,0,113,0,116,0,122,0,127,0,131,0,132,0,133,0,134,0,135,0,139,0,141,0,143,0,144,0,147,0,149,0,150,0,151,0,152,0,154,0,156,0,157,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/path_exception.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fsrc%2Fpath_exception.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/path_exception.dart","_kind":"library"},"hits":[10,0,13,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/style.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fsrc%2Fstyle.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/style.dart","_kind":"library"},"hits":[14,1,19,1,27,1,33,1,36,0,41,0,42,0,43,0,44,0,45,0,51,0,84,1]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/shared/lib/http/http_client.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fexamples%2Fshared%2Flib%2Fhttp%2Fhttp_client.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/shared/lib/http/http_client.dart","_kind":"library"},"hits":[16,0,22,0,23,0,24,0,25,0,26,0,33,0,35,1,37,0,38,0,39,0,41,0,42,0,43,0,44,0,45,0,46,0,49,0,50,0,52,0,53,0,54,0,55,0,57,0,58,0,59,0,61,0,72,0,73,0,74,0,78,0,80,0,85,0,87,0,89,0,90,0,92,0,93,0,95,0,99,0,101,0,102,0,104,0,106,0,108,0,111,0,112,0,114,0,116,0,118,0,119,0,120,0,121,0,122,0,124,0,126,0,127,0,128,0,129,0,131,0,132,0,139,0,140,0,141,0,142,0,143,0,144,0,146,0,147,0,148,0,149,0,152,0,153,0,154,0,155,1,157,0,159,1,161,0,162,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_map_stack_trace-2.1.2/lib/source_map_stack_trace.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_map_stack_trace-2.1.2%2Flib%2Fsource_map_stack_trace.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_map_stack_trace-2.1.2/lib/source_map_stack_trace.dart","_kind":"library"},"hits":[21,0,23,0,24,0,25,0,26,0,27,0,32,0,33,0,34,0,37,0,40,0,45,0,49,0,51,0,52,0,53,0,54,0,55,0,56,0,57,0,60,0,61,0,65,0,66,0,67,0,68,0,74,0,75,0,76,0,79,0,80,0,82,0,84,0,87,0,90,1,92,0,94,0,96,0,99,0,103,0,104,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/chain.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstack_trace-1.12.1%2Flib%2Fsrc%2Fchain.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/chain.dart","_kind":"library"},"hits":[19,1,51,0,77,1,82,1,87,1,88,1,95,0,96,0,97,0,98,0,101,0,107,0,109,0,111,0,112,0,114,1,143,0,144,0,146,0,147,0,150,0,151,0,152,0,153,0,154,0,164,0,165,0,166,0,167,0,168,1,169,0,176,0,177,0,178,0,179,0,181,1,182,0,184,0,186,0,187,0,190,0,216,0,218,1,219,0,221,0,222,0,227,0,228,0,229,0,234,0,237,0,238,0,244,1,247,0,249,0,250,0,251,1,252,0,253,0,257,0,258,0,259,0,260,0,261,0,262,0,263,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_maps-0.10.13/lib/parser.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_maps-0.10.13%2Flib%2Fparser.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_maps-0.10.13/lib/parser.dart","_kind":"library"},"hits":[37,0,45,0,47,0,48,0,50,0,51,0,58,0,60,0,61,0,65,0,66,0,67,0,68,0,69,0,72,0,75,0,76,0,99,0,102,0,106,0,109,0,111,0,112,0,113,0,115,0,116,0,118,0,119,0,121,0,122,0,124,0,125,0,127,0,128,0,130,0,133,0,138,0,139,0,141,0,145,0,147,0,149,0,150,0,151,0,152,0,154,0,155,0,158,0,162,0,163,0,164,0,166,0,169,0,171,0,174,0,176,0,178,0,182,0,183,0,187,0,191,0,192,0,193,0,195,0,202,0,203,0,210,0,211,0,212,0,213,0,215,0,216,0,221,0,224,0,238,0,239,0,240,0,241,0,242,0,243,0,244,0,248,0,258,0,259,0,260,0,261,0,346,0,347,0,348,0,349,0,350,0,351,0,352,0,353,0,354,0,355,0,356,0,357,0,358,0,359,0,360,0,361,0,370,0,371,0,374,0,376,0,377,0,379,0,396,0,398,0,399,0,402,0,403,0,404,0,406,0,408,0,410,0,411,0,414,0,415,0,417,0,421,0,424,0,427,0,428,0,429,0,430,0,499,0,500,0,501,0,506,0,507,1,508,0,509,0,516,0,517,0,518,0,519,0,520,1,521,0,522,0,525,0,527,0,528,0,530,0,531,0,533,0,534,0,538,0,539,0,550,0,551,0,552,0,553,0,556,0,557,0,559,0,562,0,565,0,566,0,568,0,570,0,572,0,574,0,576,0,579,0,621,0,624,0,635,0,642,0,643,0,651,0,653,0,657,0,660,0,661,0,662,0,664,0,666,0,667,0,668,0,669,0,670,0,671,0,672,0,674,0,686,0,688,0,689,0,692,0,693,0,696,0,698,0,699,0,701,0,702,0,703,0,714,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_maps-0.10.13/lib/src/source_map_span.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_maps-0.10.13%2Flib%2Fsrc%2Fsource_map_span.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_maps-0.10.13/lib/src/source_map_span.dart","_kind":"library"},"hits":[17,0,18,0,24,0,27,0,28,0,29,0,30,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_maps-0.10.13/lib/src/vlq.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_maps-0.10.13%2Flib%2Fsrc%2Fvlq.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_maps-0.10.13/lib/src/vlq.dart","_kind":"library"},"hits":[28,1,29,0,30,0,31,0,33,0,34,0,36,1,37,1,66,0,67,0,70,0,71,0,72,0,73,0,74,0,75,0,77,0,79,0,80,0,92,0,96,0,97,0,100,0,101,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/file.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_span-1.10.1%2Flib%2Fsrc%2Ffile.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/file.dart","_kind":"library"},"hits":[33,1,47,1,50,0,69,1,70,1,81,1,82,1,83,1,84,1,85,1,86,1,88,0,89,0,91,1,93,1,98,1,100,1,101,1,107,0,108,0,109,0,111,0,112,0,115,0,116,0,118,0,120,0,121,0,122,0,128,0,129,0,133,0,136,0,137,0,138,0,142,0,143,0,144,0,145,0,148,0,149,0,154,0,156,0,157,0,158,0,159,0,162,0,166,0,167,0,173,0,174,0,175,0,177,0,178,0,182,0,190,0,191,0,192,0,195,0,196,0,201,0,202,0,204,0,205,0,206,0,207,0,208,0,213,0,214,0,215,0,216,0,219,0,220,0,226,0,243,0,246,1,249,0,251,0,252,0,253,0,255,0,256,0,258,0,311,0,314,0,317,1,320,0,323,0,326,0,327,0,328,0,339,0,342,0,353,0,356,0,357,0,359,1,360,1,361,0,363,0,364,0,365,1,366,0,368,1,371,0,372,0,374,0,375,0,376,0,398,0,399,0,401,0,404,0,405,0,406,0,407,0,410,0,417,0,418,0,419,0,420,0,424,0,425,0,426,0,432,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/highlighter.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_span-1.10.1%2Flib%2Fsrc%2Fhighlighter.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/highlighter.dart","_kind":"library"},"hits":[61,0,62,0,63,0,64,0,65,0,66,0,101,0,104,0,107,0,108,0,109,0,110,0,111,1,115,0,116,0,117,0,118,0,119,0,120,0,121,0,124,0,125,0,129,0,133,0,134,0,135,0,136,0,137,0,140,0,141,0,142,0,146,0,147,0,148,0,151,0,152,0,155,0,157,0,158,0,160,0,161,0,163,0,168,0,170,0,172,1,174,0,175,0,176,0,177,0,179,0,181,0,184,0,185,0,186,0,191,0,192,0,199,0,201,0,202,0,203,0,204,0,205,0,206,0,208,0,209,0,210,0,219,0,220,0,221,0,222,0,223,0,228,0,230,0,231,0,234,0,235,0,237,0,238,0,240,0,241,0,243,0,244,0,245,0,246,0,248,0,250,0,254,0,255,0,256,0,261,0,262,0,263,0,267,0,268,0,269,0,271,0,272,0,274,0,276,0,277,0,285,0,290,0,293,0,296,0,299,0,300,0,301,0,302,0,303,0,304,0,305,0,306,0,309,0,310,0,311,0,312,0,313,0,314,0,315,0,316,0,317,0,319,0,322,0,323,0,324,0,325,0,326,0,327,0,328,0,329,0,330,0,331,0,332,0,333,0,334,0,335,0,336,0,340,0,341,0,342,0,344,0,347,0,351,0,353,0,354,0,356,0,357,0,363,0,365,0,366,0,367,0,368,0,369,0,372,0,373,0,374,0,376,0,377,0,378,0,379,0,380,0,381,0,383,0,384,0,385,0,386,1,389,0,390,0,392,0,393,0,396,0,397,0,398,0,400,0,401,0,402,0,403,0,405,0,408,0,409,0,410,0,411,0,413,0,418,0,419,0,420,0,422,0,423,0,427,0,428,0,429,0,433,0,434,0,441,0,443,0,444,0,445,0,447,0,460,0,464,0,465,0,488,0,492,0,493,0,494,0,495,0,497,0,500,0,507,0,508,0,512,0,513,0,514,0,515,0,516,0,517,0,518,0,521,0,522,0,523,0,524,0,526,0,527,0,530,0,531,0,532,0,534,0,535,0,539,0,540,0,541,0,542,0,543,0,544,0,566,0,567,0,568,0,569,0,570,0,571,0,572,0,574,0,583,0,584,0,586,0,587,0,588,0,589,0,590,0,591,0,592,0,593,0,594,0,598,0,599,0,600,0,602,0,603,0,604,0,605,0,609,0,610,0,611,0,612,0,613,0,614,0,616,0,617,0,623,0,625,0,629,0,631,0,632,0,633,0,634,0,635,0,636,0,640,0,641,0,642,0,643,0,644,0,647,0,648,0,652,0,653,0,654,0,656,0,658,0,659,0,660,0,661,0,662,0,663,0,667,0,668,0,669,0,670,0,674,0,675,0,676,0,677,0,678,0,680,0,682,0,684,0,688,0,689,0,690,0,691,0,694,0,697,0,698,0,700,0,701,0,721,0,723,0,726,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/location.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_span-1.10.1%2Flib%2Fsrc%2Flocation.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/location.dart","_kind":"library"},"hits":[35,0,36,0,45,0,47,0,50,0,51,0,52,0,53,0,54,0,55,0,57,0,62,0,63,0,64,0,65,0,67,0,68,0,77,0,78,0,79,0,80,0,82,0,83,0,86,0,87,0,88,0,89,0,92,0,95,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/span_exception.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_span-1.10.1%2Flib%2Fsrc%2Fspan_exception.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/span_exception.dart","_kind":"library"},"hits":[11,0,18,0,31,0,33,1,34,0,46,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/span_with_context.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_span-1.10.1%2Flib%2Fsrc%2Fspan_with_context.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/span_with_context.dart","_kind":"library"},"hits":[13,0,24,0,27,0,28,0,31,0,32,0,33,0,35,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/stack_zone_specification.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstack_trace-1.12.1%2Flib%2Fsrc%2Fstack_zone_specification.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/stack_zone_specification.dart","_kind":"library"},"hits":[37,1,40,0,64,0,70,0,71,0,72,0,73,0,80,0,87,0,88,0,91,0,92,0,96,0,97,1,99,0,101,1,104,0,106,0,110,0,112,0,114,0,115,0,119,0,124,0,126,0,127,1,128,0,132,0,134,0,137,0,138,1,139,0,173,0,175,0,178,0,179,0,181,0,184,0,185,0,186,0,195,0,203,0,204,0,205,0,207,0,208,0,212,0,213,0,215,0,217,0,221,0,222,0,223,0,224,0,225,0,228,0,230,0,231,0,235,0,236,0,237,0,238,0,239,0,250,0,253,0,254,0,256,0,257,0,258,0,260,0,261,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/lazy_chain.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstack_trace-1.12.1%2Flib%2Fsrc%2Flazy_chain.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/lazy_chain.dart","_kind":"library"},"hits":[18,0,20,0,23,0,27,0,28,1,30,0,32,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/frame.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstack_trace-1.12.1%2Flib%2Fsrc%2Fframe.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/frame.dart","_kind":"library"},"hits":[13,1,20,1,21,0,29,1,50,1,58,1,59,0,63,1,64,0,71,1,115,1,116,0,135,1,136,0,142,1,146,1,148,1,175,1,182,0,183,0,184,0,185,0,189,0,190,0,191,0,192,0,195,0,196,0,197,0,198,0,199,0,216,0,219,0,220,0,223,0,224,0,228,0,229,0,231,0,232,0,233,0,235,0,237,0,239,0,240,0,241,0,244,0,247,0,248,0,249,0,250,0,251,0,253,0,254,0,257,0,258,0,261,0,262,0,263,0,264,0,265,0,268,0,269,0,272,0,273,0,275,0,276,0,278,0,279,0,280,0,283,0,287,0,288,0,289,0,296,0,300,0,301,0,320,0,321,0,322,0,323,0,324,0,325,0,326,0,330,0,331,0,334,0,335,0,336,0,337,0,338,0,342,0,345,0,347,0,348,0,352,0,357,0,359,0,360,0,363,0,364,0,365,0,366,0,367,0,369,0,370,0,374,0,375,0,376,0,377,0,380,0,381,0,395,0,396,0,397,0,398,0,405,0,406,0,409,0,413,0,414,0,415,0,416,0,419,1,422,1,426,0,427,0,428,0,429,0,430,0,431,0,432,0,438,0,439,0,440,0,446,0,447,0,448,0,449,0,450,0,452,0,454,0,457,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/unparsed_frame.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstack_trace-1.12.1%2Flib%2Fsrc%2Funparsed_frame.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/unparsed_frame.dart","_kind":"library"},"hits":[12,0,29,0,32,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/lazy_trace.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstack_trace-1.12.1%2Flib%2Fsrc%2Flazy_trace.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/lazy_trace.dart","_kind":"library"},"hits":[16,0,18,0,21,0,23,0,29,0,30,1,32,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/trace.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstack_trace-1.12.1%2Flib%2Fsrc%2Ftrace.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/trace.dart","_kind":"library"},"hits":[14,1,22,1,28,1,40,1,55,1,67,1,68,0,93,0,99,0,100,0,101,0,104,0,105,0,107,0,113,0,114,0,115,0,116,1,117,0,124,0,125,0,126,0,127,0,128,0,129,0,130,0,131,0,133,0,134,0,135,0,141,0,142,0,143,0,145,0,148,0,150,0,153,0,155,0,156,0,157,1,160,0,163,0,166,0,167,0,170,0,171,0,174,0,176,0,182,1,183,0,184,0,187,0,189,0,191,1,192,0,193,0,202,0,204,0,205,0,206,0,207,0,208,0,209,0,233,0,236,0,237,0,238,0,239,0,241,1,242,0,243,0,246,0,247,0,248,0,284,0,285,0,287,0,288,0,290,0,291,0,300,0,301,0,302,0,305,0,306,0,307,0,308,0,309,0,310,0,315,0,316,0,317,0,318,0,319,0,321,0,322,0,326,0,327,0,330,0,333,1,336,0,337,0,338,0,339,0,340,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/src/guarantee_channel.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstream_channel-2.1.4%2Flib%2Fsrc%2Fguarantee_channel.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/src/guarantee_channel.dart","_kind":"library"},"hits":[16,1,19,1,20,1,27,1,35,1,37,1,46,1,47,1,50,1,52,1,53,1,54,0,55,0,56,0,57,1,59,1,65,0,66,0,67,0,68,0,69,0,70,0,85,0,106,1,114,1,118,1,119,1,121,0,123,1,125,1,126,1,129,0,130,0,132,0,134,0,136,0,137,0,143,0,144,0,145,0,146,0,149,0,152,0,153,0,157,0,158,0,161,1,162,1,164,0,166,1,168,1,169,1,170,1,171,1,172,0,173,0,174,0,175,1,178,0,179,0,180,0,183,0,184,0,186,0,187,0,188,0,191,0,192,0,198,0,199,0,200,0,202,0,203,0,204,0,205,0,206,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/src/multi_channel.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstream_channel-2.1.4%2Flib%2Fsrc%2Fmulti_channel.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/src/multi_channel.dart","_kind":"library"},"hits":[63,1,94,1,96,1,99,1,103,1,107,1,111,1,135,1,138,1,139,1,140,1,141,1,143,1,144,1,148,1,150,1,154,0,155,0,156,0,158,1,159,1,165,0,167,1,168,1,169,1,170,1,173,1,174,1,176,1,180,1,181,1,186,1,187,1,188,1,193,1,194,0,195,0,198,1,199,1,202,0,203,1,204,1,205,0,207,1,208,1,211,1,212,1,213,1,214,1,215,1,216,1,220,0,221,0,222,0,223,0,225,0,229,0,230,0,231,0,234,0,235,0,236,0,237,0,241,0,242,0,244,0,245,0,270,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/src/stream_channel_controller.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstream_channel-2.1.4%2Flib%2Fsrc%2Fstream_channel_controller.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/src/stream_channel_controller.dart","_kind":"library"},"hits":[39,1,46,1,58,1,59,1,60,1,66,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/stream_channel.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstream_channel-2.1.4%2Flib%2Fstream_channel.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/stream_channel.dart","_kind":"library"},"hits":[88,1,146,1,154,1,155,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/exception.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstring_scanner-1.4.1%2Flib%2Fsrc%2Fexception.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/exception.dart","_kind":"library"},"hits":[19,1,20,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/span_scanner.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstring_scanner-1.4.1%2Flib%2Fsrc%2Fspan_scanner.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/span_scanner.dart","_kind":"library"},"hits":[45,1,46,1,62,1,63,1,89,1,91,1,92,1,106,1,107,1,108,1,109,1,112,1,113,1,114,1,117,0,118,0,124,0,125,0,126,0,141,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test-1.28.0/lib/src/bootstrap/browser.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest-1.28.0%2Flib%2Fsrc%2Fbootstrap%2Fbrowser.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test-1.28.0/lib/src/bootstrap/browser.dart","_kind":"library"},"hits":[12,1,19,1,20,1,21,1,22,1,23,1,25,1,26,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/plugin/remote_platform_helpers.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Frunner%2Fplugin%2Fremote_platform_helpers.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/plugin/remote_platform_helpers.dart","_kind":"library"},"hits":[32,1,46,0,51,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test-1.28.0/lib/src/runner/browser/dom.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest-1.28.0%2Flib%2Fsrc%2Frunner%2Fbrowser%2Fdom.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test-1.28.0/lib/src/runner/browser/dom.dart","_kind":"library"},"hits":[14,1,32,1,97,1,111,0,157,1,159,1,160,1,202,1,204,1,205,1,206,1,207,0,210,1,211,1,218,1,222,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test-1.28.0/lib/src/runner/browser/post_message_channel.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest-1.28.0%2Flib%2Fsrc%2Frunner%2Fbrowser%2Fpost_message_channel.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test-1.28.0/lib/src/runner/browser/post_message_channel.dart","_kind":"library"},"hits":[15,1,17,1,19,1,22,1,25,1,26,1,29,1,30,1,31,1,34,1,35,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/compiler.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fcompiler.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/compiler.dart","_kind":"library"},"hits":[6,0,44,1,45,1,46,1,53,0,57,1,59,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/declarer.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fdeclarer.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/declarer.dart","_kind":"library"},"hits":[58,1,61,1,64,1,80,1,94,1,100,1,103,1,117,1,154,1,166,1,167,0,173,0,175,1,177,1,189,1,194,1,195,1,198,1,210,1,213,0,217,1,223,1,225,1,226,1,227,1,231,1,232,1,234,1,236,1,238,1,244,1,245,1,246,0,250,1,251,1,252,1,253,1,254,1,257,1,259,1,260,1,269,1,272,1,284,1,287,1,291,1,297,1,299,1,300,1,301,1,303,1,312,1,313,1,314,1,316,1,319,1,320,1,321,0,322,1,323,1,325,1,326,0,328,1,331,1,335,1,336,1,347,1,348,1,349,1,350,1,364,0,370,1,371,1,373,1,375,1,376,1,378,0,379,0,383,0,386,1,387,1,389,1,390,1,392,1,393,1,394,1,395,1,396,1,398,1,403,1,404,1,406,0,418,1,422,1,428,1,429,1,431,1,432,1,435,1,436,1,438,1,439,1,440,1,441,1,442,1,443,1,446,1,448,1,449,1,450,1,454,1,457,1,460,1,462,1,463,1,464,1,465,1,466,1,467,1,470,1,472,1,478,1,480,1,481,1,482,0,484,1,485,1,492,0,495,0,496,0,499,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/group.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fgroup.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/group.dart","_kind":"library"},"hits":[21,1,36,0,61,1,69,1,71,1,72,1,73,1,75,1,76,1,77,1,78,1,79,1,82,1,83,1,84,1,85,1,86,0,87,1,88,1,91,1,92,1,93,1,94,1,96,1,132,1,133,1,134,1,137,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/live_test_controller.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Flive_test_controller.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/live_test_controller.dart","_kind":"library"},"hits":[44,1,51,1,59,1,67,1,75,1,83,1,101,1,107,1,114,0,115,0,119,0,121,0,122,0,123,0,130,1,131,1,132,1,134,1,135,1,136,1,139,0,140,0,141,0,145,0,147,0,150,1,151,1,152,0,154,0,159,1,161,1,162,1,163,1,169,1,172,0,173,0,175,0,176,0,178,0,179,0,181,0,184,0,185,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/metadata.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fmetadata.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/metadata.dart","_kind":"library"},"hits":[24,0,33,0,40,1,44,1,51,0,76,1,79,1,96,1,137,1,138,1,153,1,162,1,176,1,177,1,178,1,179,1,180,1,181,1,182,1,183,1,184,1,185,1,186,1,187,1,188,1,192,1,193,1,194,1,199,1,200,1,201,0,202,0,203,0,205,1,206,0,207,1,212,1,224,1,225,1,230,1,232,1,233,1,234,1,235,1,236,1,242,1,254,1,256,1,262,1,263,1,264,1,269,1,271,1,272,1,275,1,277,1,278,0,279,1,280,1,281,1,282,1,283,1,284,1,285,1,286,1,287,1,288,1,289,0,290,0,293,1,294,1,295,0,296,0,297,0,299,1,302,1,303,1,304,1,305,1,306,0,307,0,309,1,313,1,315,1,316,1,317,1,320,1,322,0,323,0,324,0,327,1,332,1,333,1,334,1,335,0,336,0,337,0,338,1,345,1,346,1,347,1,348,1,349,1,350,1,351,1,352,1,353,1,354,1,355,1,356,1,357,1,359,1,360,1,361,1,362,1,365,1,366,1,369,1,382,1,383,0,384,1,385,1,386,1,387,1,388,1,389,1,390,1,391,1,392,1,393,1,406,1,410,1,411,1,413,0,414,0,415,0,416,0,417,0,418,0,419,1,423,1,425,1,426,1,427,0,428,0,430,1,431,1,432,1,433,1,434,1,435,1,436,1,437,1,438,1,440,1,441,1,442,0,444,1,446,1,449,1,450,1,451,1,452,1,453,1,455,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/configuration/timeout.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fconfiguration%2Ftimeout.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/configuration/timeout.dart","_kind":"library"},"hits":[48,0,51,1,123,1,124,1,125,1,126,1,127,1,128,1,133,1,134,1,135,1,136,1,139,0,142,1,143,1,144,1,145,1,148,0,149,0,150,0,151,0,152,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/operating_system.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Foperating_system.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/operating_system.dart","_kind":"library"},"hits":[44,1,45,1,46,1,47,1,69,0,74,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/platform_selector.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fplatform_selector.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/platform_selector.dart","_kind":"library"},"hits":[14,0,22,0,23,0,24,0,47,1,48,1,49,1,52,1,54,0,60,1,61,1,68,1,73,1,74,1,76,0,77,1,78,0,79,1,80,0,81,0,84,1,87,1,88,1,89,0,90,1,91,1,92,1,93,1,95,1,96,1,97,1,98,0,99,0,100,0,101,0,102,0,103,1,104,1,108,1,109,1,110,0,111,1,114,1,117,0,118,1,121,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/remote_listener.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fremote_listener.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/remote_listener.dart","_kind":"library"},"hits":[46,1,54,1,60,1,64,1,65,0,66,0,67,0,71,1,72,1,73,1,74,1,75,0,76,1,77,0,78,0,79,0,82,0,83,0,84,0,85,0,88,1,89,0,90,0,93,0,96,1,97,1,98,1,100,1,101,1,102,0,103,0,106,1,107,1,108,1,109,1,111,1,113,1,114,1,115,1,116,1,118,1,119,1,121,1,122,1,125,1,127,1,128,1,129,1,133,1,136,1,138,1,139,1,144,1,145,1,146,1,147,1,150,1,151,1,152,1,153,1,155,1,159,1,161,1,162,1,163,0,164,0,165,1,167,1,169,1,170,1,174,1,176,1,177,0,178,1,183,0,184,0,185,0,188,0,194,0,196,0,198,0,204,0,206,1,211,1,213,1,220,1,225,1,226,1,228,1,229,1,231,1,233,0,235,0,238,1,239,1,241,1,242,1,243,1,244,1,245,1,247,1,253,1,258,1,260,1,261,1,262,1,263,1,264,1,265,1,267,1,269,1,271,1,272,1,274,1,276,0,278,0,281,1,283,1,286,1,287,1,288,0,289,0,290,0,292,1,293,1,295,1,296,1,298,1,300,1,301,0,303,0,304,0,305,0,306,0,307,0,311,0,313,1,314,0,315,0,317,0,318,0,320,0,322,1,323,1,324,1,325,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/suite_channel_manager.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fsuite_channel_manager.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/suite_channel_manager.dart","_kind":"library"},"hits":[8,1,11,1,15,1,18,1,21,1,22,1,23,0,24,1,25,0,27,1,29,1,30,1,32,1,36,1,37,1,38,0,39,0,41,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/runtime.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fruntime.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/runtime.dart","_kind":"library"},"hits":[116,0,127,0,133,0,134,0,135,0,136,0,140,1,141,1,142,1,143,1,147,0,148,0,149,0,150,0,151,0,153,1,154,1,155,0,158,0,159,0,164,0,169,0,173,0,178,0,179,0,180,0,181,0,183,1,238,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/suite.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fsuite.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/suite.dart","_kind":"library"},"hits":[39,1,44,1,45,1,46,1,47,0,48,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/suite_platform.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fsuite_platform.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/suite_platform.dart","_kind":"library"},"hits":[34,1,40,0,41,1,42,0,44,1,45,0,46,0,49,1,54,1,55,1,56,1,58,1,59,1,61,1,62,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/scaffolding/utils.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fscaffolding%2Futils.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/scaffolding/utils.dart","_kind":"library"},"hits":[16,0,17,0,19,1,20,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/engine.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Frunner%2Fengine.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/engine.dart","_kind":"library"},"hits":[70,0,92,0,93,0,100,0,101,0,102,0,103,0,104,0,105,0,107,0,113,0,122,0,123,0,130,0,160,0,161,0,165,0,170,0,174,0,178,0,182,0,186,0,187,0,192,0,198,0,221,0,231,0,232,0,233,0,234,0,235,0,236,0,237,0,239,0,240,0,276,0,277,0,278,0,280,0,282,0,283,0,284,0,285,0,286,0,288,0,289,1,297,0,300,0,302,0,303,0,311,0,313,0,314,0,315,0,316,0,317,0,318,0,319,0,324,0,325,0,327,0,328,0,336,0,341,0,343,0,344,0,345,0,346,0,347,0,351,0,352,0,355,0,357,0,364,0,365,0,367,0,368,0,369,0,370,0,372,0,373,0,383,0,384,0,388,0,389,0,392,0,394,0,400,0,405,0,406,0,408,0,409,0,410,0,411,0,412,0,413,0,414,0,415,0,416,0,417,0,419,0,423,0,427,0,429,0,433,0,435,0,440,0,441,0,447,0,452,0,454,0,455,0,456,0,457,0,458,0,461,0,462,0,463,0,465,0,466,0,467,0,469,0,470,0,475,0,476,0,477,0,478,0,482,0,483,0,554,0,555,0,557,0,558,0,559,0,560,0,561,0,616,0,617,0,618,0,619,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/stream_group.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fstream_group.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/stream_group.dart","_kind":"library"},"hits":[32,0,42,0,85,0,120,0,122,0,138,0,139,0,140,0,143,0,144,0,145,0,149,0,151,1,154,0,155,0,169,0,170,0,174,0,176,0,186,0,187,0,189,0,193,0,195,0,197,0,202,0,203,0,206,0,227,0,228,0,230,0,231,0,232,0,234,0,235,0,237,0,239,0,243,0,251,0,252,0,257,0,258,0,260,0,265,0,266,0,267,0,268,0,269,0,274,0,275,0,276,0,277,0,278,0,279,0,288,0,289,0,291,0,292,0,294,0,295,0,335,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/union_set_controller.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fcollection-1.19.1%2Flib%2Fsrc%2Funion_set_controller.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/union_set_controller.dart","_kind":"library"},"hits":[36,0,47,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/pool-1.5.2/lib/pool.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpool-1.5.2%2Flib%2Fpool.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/pool-1.5.2/lib/pool.dart","_kind":"library"},"hits":[24,0,30,0,37,0,66,0,73,0,81,0,98,0,99,0,100,0,103,0,104,0,105,0,106,0,107,0,110,0,111,0,112,0,114,0,242,0,244,0,245,0,247,0,249,0,250,0,251,0,254,0,255,0,257,0,258,0,259,0,278,0,279,0,281,0,282,0,283,0,285,0,287,0,290,0,291,1,293,0,300,0,301,0,302,0,303,0,304,0,305,0,308,0,309,0,310,0,313,0,314,0,316,0,321,0,349,0,374,0,375,0,377,0,378,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/async_memoizer.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fasync_memoizer.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/async_memoizer.dart","_kind":"library"},"hits":[29,0,33,0,37,0,42,0,43,0,44,0,45,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/future_group.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Ffuture_group.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/future_group.dart","_kind":"library"},"hits":[21,0,35,0,69,0,70,0,75,0,76,0,79,0,80,0,83,0,85,0,89,0,91,0,92,0,93,0,94,0,95,0,96,0,101,0,102,0,103,0,104,0,105,0,106,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/union_set.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fcollection-1.19.1%2Flib%2Fsrc%2Funion_set.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/union_set.dart","_kind":"library"},"hits":[32,0,46,0,50,0,51,0,52,0,55,0,61,0,62,0,63,0,64,0,67,0,79,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/live_suite_controller.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Frunner%2Flive_suite_controller.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/live_suite_controller.dart","_kind":"library"},"hits":[21,0,34,0,37,0,40,0,43,0,48,0,61,0,82,0,85,0,88,0,99,0,108,0,109,0,110,0,113,0,114,0,116,0,118,0,119,0,120,0,122,0,123,0,124,0,125,0,126,0,127,0,128,0,130,0,132,0,134,0,136,0,137,0,142,0,143,0,147,0,148,0,149,0,151,0,153,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/suite.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Frunner%2Fsuite.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/suite.dart","_kind":"library"},"hits":[47,0,71,0,80,0,145,0,270,0,288,0,289,0,290,0,291,0,292,0,294,0,322,0,323,0,327,0,330,0,331,0,333,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/scaffolding.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Fscaffolding.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/scaffolding.dart","_kind":"library"},"hits":[38,1,40,1,41,0,47,0,49,0,50,0,54,0,55,0,56,0,57,0,60,0,71,0,72,1,73,0,75,0,76,0,78,0,80,0,81,1,144,1,157,1,173,1,175,1,231,1,244,1,260,1,262,1,275,1,306,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/util/stack_trace_mapper.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Futil%2Fstack_trace_mapper.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/util/stack_trace_mapper.dart","_kind":"library"},"hits":[28,1,40,0,41,0,44,0,45,0,62,1,64,1,65,1,68,1,69,1,70,1,72,1,74,1,87,1,91,1,92,1]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/test/login_screen_test.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fexamples%2Fmobile%2Ftest%2Flogin_screen_test.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/test/login_screen_test.dart","_kind":"library"},"hits":[20,1,21,1,25,1,26,1,27,1,29,1,30,1,31,1,33,1,34,1,36,1,37,1,38,1,40,1,42,1,45,1,48,1,50,1,53,1,55,1,56,1,59,1,61,1,63,0,64,1,66,1,67,0,68,0,71,0,74,0,75,0,78,0,80,0,81,0,83,0,84,0,86,1,87,0,89,0,92,0,93,0,96,0,98,0,100,0,101,0,103,1,104,0,105,0,107,0,111,0,114,0,115,0,118,0,120,0,122,0,123,0,125,1,126,0,127,0,130,0,133,0,134,0,137,0,139,0,141,0,142,0,144,1,145,0,147,0,150,0,152,0,153,0,156,0,158,0,160,0,161,0,162,1,166,1,167,1,168,0,169,0,171,0,173,0,176,0,179,0,181,0,184,0,185,0,186,0,189,0,191,0,193,0,194,0,196,1,197,0,198,0,201,0,204,0,206,0,209,0,210,0,211,0,214,0,216,0,218,0,219,0,221,1,222,0,224,0,227,0,229,0,232,0,233,0,234,0,237,0,239,0,241,0,242,0,244,1,245,0,246,0,248,0,252,0,255,0,258,0,259,0,260,0,263,0,266,0,268,0,269,0,271,1,272,0,273,0,276,0,279,0,282,0,283,0,284,0,287,0,290,0,292,0,293,0,294,1,298,1,299,1,302,0,305,0,306,0,309,0,311,0,312,0,313,0,315,1,316,0,317,0,319,0,321,0,323,0,324,0,325,0,330,0,332,0,333,0,335,0,336,0,338,1,339,0,340,0,342,0,344,0,347,0,349,0,351,0,352,0,354,1,355,0,356,0,358,0,360,0,363,0,365,0,367,0,368,0,370,1,371,0,372,0,374,0,376,0,379,0,381,0,383,0,384,0,386,1,387,0,388,0,390,0,392,0,395,0,397,0,399,0,400,0,402,1,403,1,404,0,410,0,411,0,413,0,416,0,418,0,420,0,421,0,423,0,426,0,427,0,430,0,432,0,433,0,435,0,437,0,438,0,440,1,441,0,442,0,444,0,446,0,449,0,451,0,456,0,457,0,458,0,459,0,463,0,464,0,466,0,467,0,469,1,470,0,471,0,473,0,475,0,477,0,478,0,481,0,484,0,486,0,492,0,494,0,495,0,500,0,502,0,503,0,505,1,506,0,507,0,509,0,511,0,513,0,514,0,517,0,520,0,522,0,525,0,527,0,528,0,532,0,534,0,535,0,537,1,538,1,539,0,545,0,546,0,548,0,551,0,552,0,554,0,555,0,559,0,560,0,561,0,563,0,564,0,566,0,569,0,570,0,573,0,575,0,578,0,580,0,581,0,585,0,586,0,588,0,589,0,591,1,592,0,593,0,595,0,597,0,599,0,600,0,603,0,606,0,608,0,612,0,613,0,614,0,615,0,620,0,621,0,623,0,624,0,626,1,627,0,628,0,630,0,632,0,634,0,635,0,638,0,641,0,643,0,646,0,647,0,648,0,649,0,654,0,656,0,657,0,659,1,660,1,661,0,667,0,668,0,670,0,673,0,674,0,676,0,677,0,681,0,682,0,683,0,685,0,686,0,688,0,691,0,692,0,695,0,697,0,700,0,701,0,702,0,703,0,707,0,708,0,710,0,711,0,713,1,714,0,715,0,717,0,719,0,722,0,724,0,728,0,730,0,731,0,736,0,737,0,739,0,740,0,742,1,743,0,744,0,746,0,748,0,751,0,755,0,757,0,758,0,762,0,766,0,768,0,769,0,774,0,775,0,777,0,778,0,780,1,781,0,782,0,784,0,786,0,787,0,789,0,793,0,797,0,799,0,800,0,804,0,808,0,812,0,814,0,815,0,819,0,821,0,823,0,824,0,826,1,827,0,828,0,830,0,832,0,835,0,839,0,841,0,842,0,846,0,850,0,852,0,853,0,858,0,859,0,861,0,862,0,864,1,865,0,866,0,868,0,870,0,871,0,874,0,877,0,879,0,880,0,884,0,887,0,890,0,892,0,893,0,897,0,899,0,900,0,902,1,903,1,904,0,910,0,911,0,913,0,916,0,917,0,918,0,922,0,923,0,925,0,926,0,928,0,931,0,932,0,935,0,937,0,940,0,942,0,943,0,947,0,950,0,953,0,955,0,956,0,960,0,961,0,963,0,964,0,965,1,969,1,970,1,971,0,972,0,974,0,976,0,979,0,982,0,983,0,986,0,988,0,991,0,996,0,998,0,999,0,1001,1,1002,0,1003,0,1005,0,1007,0,1009,0,1010,0,1015,0,1018,0,1019,0,1022,0,1024,0,1027,0,1032,0,1034,0,1035,0,1037,1,1038,0,1039,0,1041,0,1043,0,1045,0,1046,0,1051,0,1054,0,1055,0,1058,0,1060,0,1063,0,1068,0,1070,0,1071,0,1073,1,1074,0,1075,0,1077,0,1079,0,1081,0,1082,0,1087,0,1090,0,1091,0,1094,0,1096,0,1099,0,1105,0,1106,0,1108,0,1109,0,1111,1,1112,0,1113,0,1115,0,1117,0,1119,0,1120,0,1125,0,1128,0,1129,0,1132,0,1134,0,1137,0,1140,0,1141,0,1143,0,1144,0,1145,1,1149,1,1150,1,1151,0,1152,0,1153,0,1155,0,1157,0,1158,0,1159,0,1162,0,1167,0,1170,0,1176,0,1182,0,1184,1,1185,0,1187,0,1189,0,1197,0,1199,0,1201,1,1202,0,1204,0,1206,0,1210,0,1218,0,1219,1,1220,1]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/test/test_helpers.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fexamples%2Fmobile%2Ftest%2Ftest_helpers.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/test/test_helpers.dart","_kind":"library"},"hits":[13,1,18,1,19,1,20,1,21,1,22,1,25,1,26,1,27,1,28,1,29,1,30,1,32,1,33,1,34,1,36,1,38,1,39,1,40,1,41,0,42,1,43,0,44,1,45,0,46,1,47,1,48,1,49,1,53,1,54,1,55,1,56,1,58,1,59,1,60,0,61,0,62,0,63,0,64,0,65,0,66,0,67,0,68,0,69,1,70,1,78,1,89,1,101,1,102,1,104,1,105,1,106,1,107,0,111,1,112,1,113,1,114,1,117,0,118,1,120,1,121,1,122,1,123,1,124,0,125,1,128,0,129,0,136,1,137,1,138,1,139,1,140,1,142,1,143,1,144,1,147,0,148,0,149,0,151,0,154,0,155,0,160,1,161,1,162,1,167,1,173,1,174,1,175,1,177,0,178,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/path.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fpath.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/path.dart","_kind":"library"},"hits":[45,1,51,1,58,1,63,0,68,0,73,0,74,0,75,0,76,0,77,0,78,0,83,0,86,0,87,0,89,0,92,0,93,0,94,0,96,0,97,0,136,0,437,0,459,0,481,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/utils.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fsrc%2Futils.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/utils.dart","_kind":"library"},"hits":[9,0,10,0,11,0,19,0,32,0,33,0,34,0,35,0,37,0,38,0,39,0,45,0,46,0,47,0,48,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_maps-0.10.13/lib/src/utils.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_maps-0.10.13%2Flib%2Fsrc%2Futils.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_maps-0.10.13/lib/src/utils.dart","_kind":"library"},"hits":[13,0,14,0,15,0,16,0,19,0,20,0,21,0,22,0,25,0,28,0,29,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/utils.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_span-1.10.1%2Flib%2Fsrc%2Futils.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/utils.dart","_kind":"library"},"hits":[22,0,23,0,24,0,25,0,26,0,27,0,30,0,31,0,34,0,37,0,38,0,39,0,40,0,41,0,44,0,45,0,46,0,47,0,50,0,51,0,54,0,55,0,56,0,57,0,59,0,60,0,66,0,69,0,71,0,72,0,73,0,74,0,79,0,80,0,84,0,85,0,87,0,89,0,90,0,93,0,94,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/utils.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstring_scanner-1.4.1%2Flib%2Fsrc%2Futils.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/utils.dart","_kind":"library"},"hits":[8,0,10,0,15,0,16,0,17,0,18,0,27,0,28,0,31,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/remote_exception.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fremote_exception.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/remote_exception.dart","_kind":"library"},"hits":[32,0,33,0,34,0,35,0,37,0,38,0,39,0,46,0,48,0,50,0,51,0,53,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/util/pretty_print.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Futil%2Fpretty_print.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/util/pretty_print.dart","_kind":"library"},"hits":[9,0,10,0,12,0,13,0,20,0,21,0,23,0,24,0,25,0,26,0,31,0,32,0,37,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/util/pretty_print.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Futil%2Fpretty_print.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/util/pretty_print.dart","_kind":"library"},"hits":[19,0,20,0,21,0,25,0,28,0,29,0,30,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/delegate/stream_subscription.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fdelegate%2Fstream_subscription.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/delegate/stream_subscription.dart","_kind":"library"},"hits":[34,0,35,0,36,0,39,0,40,0,41,0,44,0,45,0,46,0,49,0,50,0,51,0,54,0,55,0,56,0,59,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/null_stream_sink.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fnull_stream_sink.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/null_stream_sink.dart","_kind":"library"},"hits":[46,0,59,0,60,0,61,0,69,0,70,0,72,0,73,0,74,0,75,0,76,0,77,0,81,0,82,0,83,0,84,0,86,0,89,0,90,0,91,0,92,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/result/error.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fresult%2Ferror.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/result/error.dart","_kind":"library"},"hits":[27,0,31,0,32,0,33,0,36,0,37,0,38,0,63,0,67,0,68,0,69,0,70,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/result/value.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fresult%2Fvalue.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/result/value.dart","_kind":"library"},"hits":[24,1,27,1,28,1,29,1,32,0,33,0,34,0,40,0,43,0,44,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/stream_completer.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fstream_completer.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/stream_completer.dart","_kind":"library"},"hits":[25,1,76,1,77,1,78,0,80,1,81,1,88,0,89,0,106,1,120,1,122,1,123,1,124,1,127,1,128,1,130,1,131,1,132,0,135,1,137,1,142,1,152,1,153,1,155,1,160,1,161,1,162,1,163,1,164,1,165,1,173,0,174,0,175,0,179,1,180,1,181,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/stream_queue.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fstream_queue.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/stream_queue.dart","_kind":"library"},"hits":[110,1,115,1,177,1,204,1,205,1,417,1,418,1,419,1,420,1,422,0,426,1,429,1,437,1,438,1,439,1,440,0,442,1,444,1,445,1,446,0,448,1,450,1,454,1,455,1,456,1,464,1,472,1,473,1,474,1,475,1,476,1,477,1,478,0,479,1,480,0,482,0,484,0,486,1,503,1,504,1,505,1,506,1,507,1,512,0,513,0,521,1,528,1,529,1,530,1,531,1,534,1,689,1,690,1,691,1,692,1,694,1,695,0,696,0,698,1,699,1,885,1,886,1,887,1,888,1,890,1,895,0,896,0,897,0,899,0,900,0,901,0,902,0,904,1,905,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/subscription_stream.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fsubscription_stream.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/subscription_stream.dart","_kind":"library"},"hits":[32,1,35,1,37,1,38,1,39,1,43,1,45,1,46,1,47,0,50,1,55,1,56,1,57,1,58,1,59,1,60,1,71,1,74,0,76,0,78,0,79,0,80,0,81,0,82,0,84,0,85,0,86,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/stream_sink_completer.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Fstream_sink_completer.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/stream_sink_completer.dart","_kind":"library"},"hits":[20,1,31,1,62,1,63,0,65,1,81,1,101,0,104,0,105,0,106,0,107,0,108,0,110,0,111,0,132,0,133,0,135,0,136,0,139,0,140,0,141,0,143,0,145,0,146,0,149,0,150,0,151,0,159,1,160,1,161,1,165,1,168,0,170,0,171,0,176,1,177,0,179,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/all.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fall.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/all.dart","_kind":"library"},"hits":[17,0,20,0,26,0,29,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/evaluator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fevaluator.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/evaluator.dart","_kind":"library"},"hits":[13,1,16,1,19,0,22,0,23,0,26,0,27,0,30,0,31,0,32,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/impl.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fimpl.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/impl.dart","_kind":"library"},"hits":[28,1,29,1,31,0,37,1,38,1,41,0,42,0,43,0,44,0,45,0,47,0,59,0,60,0,61,0,64,1,67,1,68,1,71,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/intersection_selector.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fintersection_selector.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/intersection_selector.dart","_kind":"library"},"hits":[19,0,22,0,23,0,26,0,27,0,33,0,34,0,35,0,36,0,39,0,42,0,43,0,44,0,45,0,48,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/validator.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fvalidator.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/validator.dart","_kind":"library"},"hits":[16,0,19,0,20,0,21,0,22,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/none.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fnone.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/none.dart","_kind":"library"},"hits":[15,0,18,0,24,0,27,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/parser.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fparser.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/parser.dart","_kind":"library"},"hits":[21,1,26,1,27,1,29,1,30,0,31,0,34,1,35,1,42,1,43,1,44,1,46,0,47,0,48,0,51,0,52,0,53,1,59,1,60,1,61,1,62,0,63,1,69,1,70,1,71,1,72,0,73,1,81,1,82,1,83,1,85,0,86,0,89,0,90,0,91,0,92,0,94,0,97,1,100,0,102,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/scanner.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fscanner.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/scanner.dart","_kind":"library"},"hits":[12,1,18,1,24,1,37,1,43,1,49,1,50,1,51,1,52,1,53,1,54,1,61,1,62,1,63,0,64,0,65,1,68,1,69,1,71,1,72,1,73,1,76,1,77,1,78,1,79,1,80,1,81,1,82,1,83,1,84,1,86,1,92,0,93,0,95,0,96,0,103,0,104,0,112,0,113,0,118,1,119,1,124,1,125,1,126,1,129,1,134,1,135,1,137,0,140,0,142,0,143,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/string_scanner.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstring_scanner-1.4.1%2Flib%2Fsrc%2Fstring_scanner.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/string_scanner.dart","_kind":"library"},"hits":[23,1,38,1,41,1,42,1,43,1,52,1,73,0,74,0,84,1,87,1,88,1,89,1,183,1,184,1,185,1,186,1,187,1,189,1,190,1,199,1,200,1,202,0,204,0,208,0,212,0,213,1,226,1,227,1,228,1,229,1,230,1,269,0,270,0,271,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/token.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Ftoken.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/token.dart","_kind":"library"},"hits":[19,1,32,1,35,0,73,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/visitor.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fboolean_selector-2.1.2%2Flib%2Fsrc%2Fvisitor.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib/src/visitor.dart","_kind":"library"},"hits":[27,0,28,0,29,0,32,0,33,0,34,0,35,0,38,0,39,0,40,0,41,0,44,0,45,0,46,0,47,0,48,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/empty_unmodifiable_set.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fcollection-1.19.1%2Flib%2Fsrc%2Fempty_unmodifiable_set.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/empty_unmodifiable_set.dart","_kind":"library"},"hits":[17,0,19,0,23,0,39,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/wrappers.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fcollection-1.19.1%2Flib%2Fsrc%2Fwrappers.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/collection-1.19.1/lib/src/wrappers.dart","_kind":"library"},"hits":[26,1,29,0,32,0,38,0,55,0,61,0,67,0,74,0,77,0,94,0,100,0,106,1,109,0,112,1,118,0,320,0,340,0,341,0,342,0,392,1,395,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/feature_matcher.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Ffeature_matcher.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/feature_matcher.dart","_kind":"library"},"hits":[15,1,16,1,21,0,27,0,28,0,36,0,37,0,39,0,44,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/interfaces.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Finterfaces.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/interfaces.dart","_kind":"library"},"hits":[57,0,62,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/operator_matchers.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Foperator_matchers.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/operator_matchers.dart","_kind":"library"},"hits":[9,0,17,0,18,1,21,0,22,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/order_matchers.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Forder_matchers.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/order_matchers.dart","_kind":"library"},"hits":[121,1,122,1,123,1,124,0,125,0,126,0,127,0,129,0,131,1,134,0,136,0,137,0,138,0,139,0,143,0,146,0,153,0,154,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/type_matcher.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fmatcher-0.12.18%2Flib%2Fsrc%2Ftype_matcher.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/matcher-0.12.18/lib/src/type_matcher.dart","_kind":"library"},"hits":[94,0,95,0,96,0,97,0,100,1,103,0,109,0,110,0,111,0,114,1,123,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/internal_style.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fsrc%2Finternal_style.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/internal_style.dart","_kind":"library"},"hits":[46,0,47,0,48,0,49,0,50,0,63,0,64,0,65,0,69,0,70,0,71,0,79,0,85,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/style/posix.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fsrc%2Fstyle%2Fposix.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/style/posix.dart","_kind":"library"},"hits":[10,0,20,0,22,0,24,0,29,1,32,0,35,0,36,0,39,0,40,0,41,0,42,0,45,0,51,0,52,0,53,0,55,0,56,0,59,0,60,0,61,0,65,0,66,0,69,0,72,0,73,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/style/url.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fsrc%2Fstyle%2Furl.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/style/url.dart","_kind":"library"},"hits":[10,0,20,0,22,0,24,0,26,0,29,1,32,0,35,0,36,0,39,0,43,0,44,0,47,0,48,0,49,0,51,0,52,0,53,0,54,0,55,0,59,0,60,0,61,0,65,0,66,0,67,0,71,0,72,0,75,0,76,0,82,0,85,0,87,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/style/windows.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fpath-1.9.1%2Flib%2Fsrc%2Fstyle%2Fwindows.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/path-1.9.1/lib/src/style/windows.dart","_kind":"library"},"hits":[15,0,25,0,27,0,29,0,31,0,34,1,37,0,38,0,41,0,42,0,43,0,44,0,47,0,48,0,49,0,50,0,51,0,54,0,55,0,56,0,57,0,59,0,63,0,65,0,67,0,69,0,70,0,71,0,74,0,84,0,85,0,86,0,89,0,90,0,94,0,95,0,99,0,101,0,102,0,105,0,106,0,107,0,112,1,113,0,115,0,118,0,121,0,122,0,130,0,131,0,136,0,137,0,139,0,141,0,144,0,145,0,148,0,149,0,153,0,156,0,157,0,158,0,161,0,162,0,163,0,164,0,165,0,166,0,169,0,170,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/restartable_timer.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fasync-2.13.0%2Flib%2Fsrc%2Frestartable_timer.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/async-2.13.0/lib/src/restartable_timer.dart","_kind":"library"},"hits":[39,0,40,0,45,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/top_level.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fterm_glyph-1.2.2%2Flib%2Fsrc%2Fgenerated%2Ftop_level.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/top_level.dart","_kind":"library"},"hits":[61,0,67,0,73,0,85,0,97,0,127,0,133,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/term_glyph.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fterm_glyph-1.2.2%2Flib%2Fterm_glyph.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/term_glyph.dart","_kind":"library"},"hits":[21,0,31,0,37,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/location_mixin.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_span-1.10.1%2Flib%2Fsrc%2Flocation_mixin.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/location_mixin.dart","_kind":"library"},"hits":[20,0,24,0,25,0,26,0,27,0,29,0,30,0,36,0,37,0,38,0,39,0,41,0,42,0,45,0,46,0,47,0,48,0,51,0,54,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/span.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_span-1.10.1%2Flib%2Fsrc%2Fspan.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/span.dart","_kind":"library"},"hits":[103,0,104,0,105,0,106,0,107,0,108,0,109,0,110,0,113,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/span_mixin.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fsource_span-1.10.1%2Flib%2Fsrc%2Fspan_mixin.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/span_mixin.dart","_kind":"library"},"hits":[20,1,23,0,26,0,27,0,28,0,29,0,53,0,54,0,55,0,56,0,57,0,59,0,66,0,67,0,70,0,71,0,72,0,73,0,76,0,77,0,80,0,83,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/ascii_glyph_set.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fterm_glyph-1.2.2%2Flib%2Fsrc%2Fgenerated%2Fascii_glyph_set.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/ascii_glyph_set.dart","_kind":"library"},"hits":[16,0,32,0,34,0,36,0,40,0,44,0,54,0,56,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/unicode_glyph_set.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fterm_glyph-1.2.2%2Flib%2Fsrc%2Fgenerated%2Funicode_glyph_set.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/unicode_glyph_set.dart","_kind":"library"},"hits":[16,0,32,0,34,0,36,0,40,0,44,0,54,0,56,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/closed_exception.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fclosed_exception.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/closed_exception.dart","_kind":"library"},"hits":[11,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/live_test.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Flive_test.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/live_test.dart","_kind":"library"},"hits":[61,1,118,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/state.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fstate.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/state.dart","_kind":"library"},"hits":[26,0,31,1,32,1,35,0,38,0,39,0,40,0,41,0,42,0,43,0,47,0,66,0,70,0,97,0,108,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/message.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Fmessage.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/message.dart","_kind":"library"},"hits":[17,0,18,0,40,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/src/stream_channel_completer.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstream_channel-2.1.4%2Flib%2Fsrc%2Fstream_channel_completer.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib/src/stream_channel_completer.dart","_kind":"library"},"hits":[23,1,42,1,53,1,54,1,56,1,57,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/test.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Ftest.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/test.dart","_kind":"library"},"hits":[24,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/util/iterable_set.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Frunner%2Futil%2Fiterable_set.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/util/iterable_set.dart","_kind":"library"},"hits":[23,0,26,0,29,0,32,0,43,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/runner_suite.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Frunner%2Frunner_suite.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/runner_suite.dart","_kind":"library"},"hits":[33,0,63,0,87,0,101,0,122,0,194,1,195,0,198,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/reporter/expanded.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Frunner%2Freporter%2Fexpanded.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/runner/reporter/expanded.dart","_kind":"library"},"hits":[89,0,129,0,133,0,159,0,160,0,161,0,163,0,164,0,167,0,168,0,169,0,173,0,178,0,179,0,180,1,192,0,193,0,194,0,198,0,199,0,200,0,201,0,202,0,203,0,204,0,206,0,209,0,210,0,214,0,215,0,217,0,220,0,221,0,223,0,226,0,228,0,231,0,232,0,233,0,234,0,244,0,250,0,251,0,255,0,257,0,258,0,259,0,260,0,261,0,262,0,263,0,266,0,268,0,270,0,273,0,274,0,282,0,289,0,291,0,292,0,293,0,294,0,296,0,297,0,300,0,301,0,302,0,303,0,304,0,306,0,307,0,308,0,313,0,315,0,316,0,319,0,321,0,326,0,328,0,337,0,338,0,342,0,343,0,350,0,351,0,367,0,368,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/util/os.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Futil%2Fos.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/util/os.dart","_kind":"library"},"hits":[25,1,26,0,27,0,28,0,29,0,30,0,32,0,33,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/util/print_sink.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_core-0.6.14%2Flib%2Fsrc%2Futil%2Fprint_sink.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_core-0.6.14/lib/src/util/print_sink.dart","_kind":"library"},"hits":[27,0,28,0,29,0,30,0,33,0,34,0,35,0,38,0]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/utils.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Fstack_trace-1.12.1%2Flib%2Fsrc%2Futils.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/stack_trace-1.12.1/lib/src/utils.dart","_kind":"library"},"hits":[11,1]},{"source":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/util/identifier_regex.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2F.pub-cache%2Fhosted%2Fpub.dev%2Ftest_api-0.7.8%2Flib%2Fsrc%2Fbackend%2Futil%2Fidentifier_regex.dart","uri":"file:///Users/christianfindlay/.pub-cache/hosted/pub.dev/test_api-0.7.8/lib/src/backend/util/identifier_regex.dart","_kind":"library"},"hits":[9,1]},{"source":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/test/types_test.dart","script":{"type":"@Script","fixedId":true,"id":"libraries/1/scripts/file%3A%2F%2F%2FUsers%2Fchristianfindlay%2FDocuments%2FCode%2Fdart_node%2Fexamples%2Fmobile%2Ftest%2Ftypes_test.dart","uri":"file:///Users/christianfindlay/Documents/Code/dart_node/examples/mobile/test/types_test.dart","_kind":"library"},"hits":[13,1,14,1,16,1,17,1,19,1,20,1,22,1,24,1,25,1,27,1,29,1,30,1,32,1,34,1,35,1,37,1,39,1,41,1,42,1,43,1,45,1,46,1,48,1,49,1,52,1,53,1,55,1,56,1,59,1,60,1,62,1,67,1,68,1,69,1,71,1,72,1,74,1,75,1,77,1,79,1,80,1,82,1,83,1,86,1,87,1,89,1,90,1,93,1,94,1,95,1,97,1,98,1,99,1,102,1,103,1,104,1,105,1,106,1,109,1,111,1,112,1,113,1,115,1,116,1,117,1,119,1,121,1,122,1,124,1,125,1,126,1,128,1,130,1,131,1,132,1,134,1,135,1,136,1,140,1,141,1,142,1,143,1,147,1,149,1,150,1,151,1,153,1,154,1,158,1,161,1,163,1,164,1,165,1,166,1]}]} \ No newline at end of file diff --git a/examples/mobile/dart_test.yaml b/examples/mobile/dart_test.yaml new file mode 100644 index 0000000..1d9e304 --- /dev/null +++ b/examples/mobile/dart_test.yaml @@ -0,0 +1,8 @@ +platforms: [chrome] + +override_platforms: + chrome: + settings: + arguments: --disable-gpu --no-sandbox + +custom_html_template_path: test/test_template.html diff --git a/examples/mobile/lib/app.dart b/examples/mobile/lib/app.dart index 3acca62..edd60ca 100644 --- a/examples/mobile/lib/app.dart +++ b/examples/mobile/lib/app.dart @@ -2,30 +2,36 @@ import 'dart:js_interop'; import 'package:dart_node_react/dart_node_react.dart'; import 'package:dart_node_react_native/dart_node_react_native.dart'; +import 'package:shared/http/http_client.dart'; import 'screens/login_screen.dart'; import 'screens/register_screen.dart'; import 'screens/task_list_screen.dart'; import 'types.dart'; -/// Main App component -JSFunction app() => createFunctionalComponent((JSObject props) { - final tokenState = useState(null); - final userState = useState(null); - final viewState = useState('login'.toJS); - - final token = tokenState.$1 as JSString?; - final user = userState.$1 as JSObject?; - final view = (viewState.$1 as JSString?)?.toDart ?? 'login'; +/// Main App component - renders as a ReactElement for testing +// React component functions follow PascalCase naming convention +// ignore: non_constant_identifier_names +ReactElement MobileApp({Fetch? fetchFn}) => + functionalComponent('MobileApp', (JSObject props) { + final tokenState = useStateJS(null); + final userState = useStateJS(null); + final viewState = useState('login'); - final setToken = wrapSetStateJSAny(tokenState.$2); - final setUser = wrapSetStateJSAny(userState.$2); - final setView = wrapSetState(viewState.$2); + final token = switch (tokenState.value) { + final JSString s => s, + _ => null, + }; + final user = switch (userState.value) { + final JSObject o => JSUser.fromJS(o), + _ => null, + }; + final view = viewState.value; final authEffects = ( - setToken: (JSString? t) => setToken(t), - setUser: (JSObject? u) => setUser(u), - setView: setView, + setToken: (JSAny? t) => tokenState.set(t), + setUser: (JSAny? u) => userState.set(u), + setView: (String v) => viewState.set(v), ); return _buildCurrentView( @@ -33,25 +39,57 @@ JSFunction app() => createFunctionalComponent((JSObject props) { token: token, user: user, authEffects: authEffects, + fetchFn: fetchFn, ); }); +/// Main App component as JSFunction for registration +JSFunction app() => createFunctionalComponent((JSObject props) { + final tokenState = useStateJS(null); + final userState = useStateJS(null); + final viewState = useState('login'); + + final token = switch (tokenState.value) { + final JSString s => s, + _ => null, + }; + final user = switch (userState.value) { + final JSObject o => JSUser.fromJS(o), + _ => null, + }; + final view = viewState.value; + + final authEffects = ( + setToken: (JSAny? t) => tokenState.set(t), + setUser: (JSAny? u) => userState.set(u), + setView: (String v) => viewState.set(v), + ); + + return _buildCurrentView( + view: view, + token: token, + user: user, + authEffects: authEffects, + ); +}); + ReactElement _buildCurrentView({ required String view, required JSString? token, - required JSObject? user, + required JSUser? user, required AuthEffects authEffects, -}) => - switch (view) { - 'login' => loginScreen(authEffects: authEffects), - 'register' => registerScreen(authEffects: authEffects), - 'tasks' => taskListScreen( - token: token?.toDart ?? '', - user: user, - authEffects: authEffects, - ), - _ => loginScreen(authEffects: authEffects), - }; + Fetch? fetchFn, +}) => switch (view) { + 'login' => loginScreen(authEffects: authEffects, fetchFn: fetchFn), + 'register' => registerScreen(authEffects: authEffects, fetchFn: fetchFn), + 'tasks' => taskListScreen( + token: token?.toDart ?? '', + user: user, + authEffects: authEffects, + fetchFn: fetchFn, + ), + _ => loginScreen(authEffects: authEffects, fetchFn: fetchFn), +}; @JS('console.log') external void _consoleLog(JSAny? message); diff --git a/examples/mobile/lib/screens/login_screen.dart b/examples/mobile/lib/screens/login_screen.dart index 2e94744..06985ed 100644 --- a/examples/mobile/lib/screens/login_screen.dart +++ b/examples/mobile/lib/screens/login_screen.dart @@ -1,7 +1,7 @@ import 'dart:js_interop'; import 'dart:js_interop_unsafe'; -import 'package:dart_node_react/dart_node_react.dart'; +import 'package:dart_node_react/dart_node_react.dart' hide view; import 'package:dart_node_react_native/dart_node_react_native.dart'; import 'package:nadz/nadz.dart'; import 'package:shared/http/http_client.dart'; @@ -10,31 +10,27 @@ import 'package:shared/theme/theme.dart'; import '../types.dart'; /// Login screen component -ReactElement loginScreen({required AuthEffects authEffects}) => +ReactElement loginScreen({required AuthEffects authEffects, Fetch? fetchFn}) => functionalComponent('LoginScreen', (JSObject props) { - final emailState = useState(''.toJS); - final passwordState = useState(''.toJS); - final loadingState = useState(false.toJS); - final errorState = useState(null); + final emailState = useState(''); + final passwordState = useState(''); + final loadingState = useState(false); + final errorState = useState(null); - final email = (emailState.$1 as JSString?)?.toDart ?? ''; - final password = (passwordState.$1 as JSString?)?.toDart ?? ''; - final loading = (loadingState.$1 as JSBoolean?)?.toDart ?? false; - final error = (errorState.$1 as JSString?)?.toDart; - - final setEmail = wrapSetState(emailState.$2); - final setPassword = wrapSetState(passwordState.$2); - final setLoading = wrapSetState(loadingState.$2); - final setError = wrapSetState(errorState.$2); + final email = emailState.value; + final password = passwordState.value; + final loading = loadingState.value; + final error = errorState.value; void handleLogin() { - setLoading(true); - setError(null); + loadingState.set(true); + errorState.set(null); _performLogin( email: email, password: password, authEffects: authEffects, - formEffects: (setLoading: setLoading, setError: setError), + formEffects: (setLoading: loadingState.set, setError: errorState.set), + fetchFn: fetchFn, ); } @@ -57,7 +53,7 @@ ReactElement loginScreen({required AuthEffects authEffects}) => textInput( placeholder: 'Enter your email', value: email, - onChangeText: setEmail, + onChangeText: emailState.set, style: AppStyles.input, props: {'placeholderTextColor': AppColors.textMuted}, ), @@ -70,7 +66,7 @@ ReactElement loginScreen({required AuthEffects authEffects}) => textInput( placeholder: 'Enter your password', value: password, - onChangeText: setPassword, + onChangeText: passwordState.set, secureTextEntry: true, style: AppStyles.input, props: {'placeholderTextColor': AppColors.textMuted}, @@ -105,26 +101,44 @@ void _performLogin({ required String password, required AuthEffects authEffects, required FormEffects formEffects, + Fetch? fetchFn, }) { - fetchJson( - '$apiUrl/auth/login', - method: 'POST', - body: {'email': email, 'password': password}, - ).then((result) { - result.match( - onSuccess: (response) { - final data = response['data'] as JSObject?; - final token = data?['token'] as JSString?; - final user = data?['user'] as JSObject?; - authEffects.setToken(token); - authEffects.setUser(user); - authEffects.setView('tasks'); - }, - onError: (message) => formEffects.setError(message), - ); - }).catchError((Object e) { - formEffects.setError(e.toString()); - }).whenComplete(() { - formEffects.setLoading(false); - }); + final doFetch = fetchFn ?? fetchJson; + doFetch( + '$apiUrl/auth/login', + method: 'POST', + body: {'email': email, 'password': password}, + ) + .then((result) { + result.match( + onSuccess: (response) { + final data = response['data']; + switch (data) { + case final JSObject d: + final token = d['token']; + final user = switch (d['user']) { + final JSObject u => u, + _ => null, + }; + switch (token) { + case final JSString t: + authEffects.setToken(t); + authEffects.setUser(user); + authEffects.setView('tasks'); + case _: + formEffects.setError('No token in response'); + } + case _: + formEffects.setError('Login failed'); + } + }, + onError: (message) => formEffects.setError(message), + ); + }) + .catchError((Object e) { + formEffects.setError(e.toString()); + }) + .whenComplete(() { + formEffects.setLoading(false); + }); } diff --git a/examples/mobile/lib/screens/register_screen.dart b/examples/mobile/lib/screens/register_screen.dart index bef1fde..a235735 100644 --- a/examples/mobile/lib/screens/register_screen.dart +++ b/examples/mobile/lib/screens/register_screen.dart @@ -1,7 +1,7 @@ import 'dart:js_interop'; import 'dart:js_interop_unsafe'; -import 'package:dart_node_react/dart_node_react.dart'; +import 'package:dart_node_react/dart_node_react.dart' hide view; import 'package:dart_node_react_native/dart_node_react_native.dart'; import 'package:nadz/nadz.dart'; import 'package:shared/http/http_client.dart'; @@ -10,112 +10,109 @@ import 'package:shared/theme/theme.dart'; import '../types.dart'; /// Register screen component -ReactElement registerScreen({required AuthEffects authEffects}) => - functionalComponent('RegisterScreen', (JSObject props) { - final nameState = useState(''.toJS); - final emailState = useState(''.toJS); - final passwordState = useState(''.toJS); - final loadingState = useState(false.toJS); - final errorState = useState(null); - - final name = (nameState.$1 as JSString?)?.toDart ?? ''; - final email = (emailState.$1 as JSString?)?.toDart ?? ''; - final password = (passwordState.$1 as JSString?)?.toDart ?? ''; - final loading = (loadingState.$1 as JSBoolean?)?.toDart ?? false; - final error = (errorState.$1 as JSString?)?.toDart; +ReactElement registerScreen({ + required AuthEffects authEffects, + Fetch? fetchFn, +}) => functionalComponent('RegisterScreen', (JSObject props) { + final nameState = useState(''); + final emailState = useState(''); + final passwordState = useState(''); + final loadingState = useState(false); + final errorState = useState(null); - final setName = wrapSetState(nameState.$2); - final setEmail = wrapSetState(emailState.$2); - final setPassword = wrapSetState(passwordState.$2); - final setLoading = wrapSetState(loadingState.$2); - final setError = wrapSetState(errorState.$2); + final name = nameState.value; + final email = emailState.value; + final password = passwordState.value; + final loading = loadingState.value; + final error = errorState.value; - void handleRegister() { - setLoading(true); - setError(null); - _performRegister( - name: name, - email: email, - password: password, - authEffects: authEffects, - formEffects: (setLoading: setLoading, setError: setError), - ); - } + void handleRegister() { + loadingState.set(true); + errorState.set(null); + _performRegister( + name: name, + email: email, + password: password, + authEffects: authEffects, + formEffects: (setLoading: loadingState.set, setError: errorState.set), + fetchFn: fetchFn, + ); + } - return view( - style: AppStyles.centeredContent, - child: view( - style: AppStyles.authCard, + return view( + style: AppStyles.centeredContent, + child: view( + style: AppStyles.authCard, + children: [ + text('Create Account', style: AppStyles.authTitle), + (error?.isNotEmpty ?? false) + ? view( + style: AppStyles.errorMsg, + child: text(error ?? '', style: AppStyles.errorText), + ) + : view(), + view( + style: AppStyles.formGroup, children: [ - text('Create Account', style: AppStyles.authTitle), - (error?.isNotEmpty ?? false) - ? view( - style: AppStyles.errorMsg, - child: text(error ?? '', style: AppStyles.errorText), - ) - : view(), - view( - style: AppStyles.formGroup, - children: [ - text('Name', style: AppStyles.label), - textInput( - placeholder: 'Enter your name', - value: name, - onChangeText: setName, - style: AppStyles.input, - props: {'placeholderTextColor': AppColors.textMuted}, - ), - ], + text('Name', style: AppStyles.label), + textInput( + placeholder: 'Enter your name', + value: name, + onChangeText: nameState.set, + style: AppStyles.input, + props: {'placeholderTextColor': AppColors.textMuted}, ), - view( - style: AppStyles.formGroup, - children: [ - text('Email', style: AppStyles.label), - textInput( - placeholder: 'Enter your email', - value: email, - onChangeText: setEmail, - style: AppStyles.input, - props: {'placeholderTextColor': AppColors.textMuted}, - ), - ], + ], + ), + view( + style: AppStyles.formGroup, + children: [ + text('Email', style: AppStyles.label), + textInput( + placeholder: 'Enter your email', + value: email, + onChangeText: emailState.set, + style: AppStyles.input, + props: {'placeholderTextColor': AppColors.textMuted}, ), - view( - style: AppStyles.formGroup, - children: [ - text('Password', style: AppStyles.label), - textInput( - placeholder: 'Enter your password', - value: password, - onChangeText: setPassword, - secureTextEntry: true, - style: AppStyles.input, - props: {'placeholderTextColor': AppColors.textMuted}, - ), - ], + ], + ), + view( + style: AppStyles.formGroup, + children: [ + text('Password', style: AppStyles.label), + textInput( + placeholder: 'Enter your password', + value: password, + onChangeText: passwordState.set, + secureTextEntry: true, + style: AppStyles.input, + props: {'placeholderTextColor': AppColors.textMuted}, ), + ], + ), + touchableOpacity( + onPress: loading ? null : handleRegister, + style: AppStyles.btnPrimary, + child: text( + loading ? 'Creating account...' : 'Register', + style: AppStyles.btnPrimaryText, + ), + ), + view( + style: AppStyles.linkContainer, + children: [ + text('Already have an account? ', style: AppStyles.linkText), touchableOpacity( - onPress: loading ? null : handleRegister, - style: AppStyles.btnPrimary, - child: text( - loading ? 'Creating account...' : 'Register', - style: AppStyles.btnPrimaryText, - ), - ), - view( - style: AppStyles.linkContainer, - children: [ - text('Already have an account? ', style: AppStyles.linkText), - touchableOpacity( - onPress: () => authEffects.setView('login'), - child: text('Sign In', style: AppStyles.linkHighlight), - ), - ], + onPress: () => authEffects.setView('login'), + child: text('Sign In', style: AppStyles.linkHighlight), ), ], ), - ); - }); + ], + ), + ); +}); void _performRegister({ required String name, @@ -123,26 +120,44 @@ void _performRegister({ required String password, required AuthEffects authEffects, required FormEffects formEffects, + Fetch? fetchFn, }) { - fetchJson( - '$apiUrl/auth/register', - method: 'POST', - body: {'name': name, 'email': email, 'password': password}, - ).then((result) { - result.match( - onSuccess: (response) { - final data = response['data'] as JSObject?; - final token = data?['token'] as JSString?; - final user = data?['user'] as JSObject?; - authEffects.setToken(token); - authEffects.setUser(user); - authEffects.setView('tasks'); - }, - onError: (message) => formEffects.setError(message), - ); - }).catchError((Object e) { - formEffects.setError(e.toString()); - }).whenComplete(() { - formEffects.setLoading(false); - }); + final doFetch = fetchFn ?? fetchJson; + doFetch( + '$apiUrl/auth/register', + method: 'POST', + body: {'name': name, 'email': email, 'password': password}, + ) + .then((result) { + result.match( + onSuccess: (response) { + final data = response['data']; + switch (data) { + case final JSObject d: + final token = switch (d['token']) { + final JSString t => t, + _ => null, + }; + final user = switch (d['user']) { + final JSObject u => u, + _ => null, + }; + authEffects.setToken(token); + authEffects.setUser(user); + authEffects.setView('tasks'); + case _: + authEffects.setToken(null); + authEffects.setUser(null); + authEffects.setView('tasks'); + } + }, + onError: (message) => formEffects.setError(message), + ); + }) + .catchError((Object e) { + formEffects.setError(e.toString()); + }) + .whenComplete(() { + formEffects.setLoading(false); + }); } diff --git a/examples/mobile/lib/screens/task_list_screen.dart b/examples/mobile/lib/screens/task_list_screen.dart index 4a39fff..020c52c 100644 --- a/examples/mobile/lib/screens/task_list_screen.dart +++ b/examples/mobile/lib/screens/task_list_screen.dart @@ -1,385 +1,384 @@ import 'dart:js_interop'; import 'dart:js_interop_unsafe'; -import 'package:dart_node_react/dart_node_react.dart'; +import 'package:dart_node_react/dart_node_react.dart' hide view; import 'package:dart_node_react_native/dart_node_react_native.dart'; import 'package:nadz/nadz.dart'; import 'package:shared/http/http_client.dart'; +import 'package:shared/js_types/js_types.dart'; import 'package:shared/theme/theme.dart'; import '../types.dart'; import '../websocket.dart'; +/// Type-safe wrapper for WebSocket task events +extension type JSTaskEvent._(JSObject _) implements JSObject { + /// Get the event type safely + String? get type => switch (_['type']) { + final JSString s => s.toDart, + _ => null, + }; + + /// Get the task data safely + JSTask? get data => switch (_['data']) { + final JSObject d => JSTask.fromJS(d), + _ => null, + }; +} + /// Task list screen component ReactElement taskListScreen({ required String token, - required JSObject? user, + required JSUser? user, required AuthEffects authEffects, -}) => - functionalComponent('TaskListScreen', (JSObject props) { - final tasksState = useState([].toJS); - final loadingState = useState(true.toJS); - final errorState = useState(null); - final showAddFormState = useState(false.toJS); - final newTaskTitleState = useState(''.toJS); - - final tasks = (tasksState.$1 as JSArray?)?.toDart ?? []; - final loading = (loadingState.$1 as JSBoolean?)?.toDart ?? true; - final error = (errorState.$1 as JSString?)?.toDart; - final showAddForm = (showAddFormState.$1 as JSBoolean?)?.toDart ?? false; - final newTaskTitle = (newTaskTitleState.$1 as JSString?)?.toDart ?? ''; + Fetch? fetchFn, +}) => functionalComponent('TaskListScreen', (JSObject props) { + final tasksState = useStateJSArray(null); + final loadingState = useState(true); + final errorState = useState(null); + final showAddFormState = useState(false); + final newTaskTitleState = useState(''); - final setTasks = tasksState.$2; - final setLoading = wrapSetState(loadingState.$2); - final setError = wrapSetState(errorState.$2); - final setShowAddForm = wrapSetState(showAddFormState.$2); - final setNewTaskTitle = wrapSetState(newTaskTitleState.$2); + final tasks = tasksState.value; + final loading = loadingState.value; + final error = errorState.value; + final showAddForm = showAddFormState.value; + final newTaskTitle = newTaskTitleState.value; - useEffect( - (() { - _loadTasks(token, setTasks, setLoading, setError); - }).toJS, - [token.toJS].toJS, - ); + useEffect(() { + _loadTasks(token, tasksState, loadingState, errorState, fetchFn); + return null; + }, [token]); - // WebSocket connection for real-time updates - useEffect( - (() { - final ws = connectWebSocket( - token: token, - onTaskEvent: (event) { - final type = (event['type'] as JSString?)?.toDart; - final data = event['data'] as JSObject?; - switch (data) { - case final JSObject d: - _handleTaskEvent(type, d, setTasks); - case null: - break; - } - }, - ); - return (() => ws?.close()).toJS; - }).toJS, - [token.toJS].toJS, - ); - - void handleLogout() { - authEffects.setToken(null); - authEffects.setUser(null); - authEffects.setView('login'); - } + // WebSocket connection for real-time updates + useEffect(() { + final ws = connectWebSocket( + token: token, + onTaskEvent: (jsEvent) { + final event = JSTaskEvent._(jsEvent); + switch (event.data) { + case final JSTask task: + _handleTaskEvent(event.type, task, tasksState); + case null: + break; + } + }, + ); + return () => ws?.close(); + }, [token]); - void handleToggle(String id, bool completed) { - _toggleTask(token, id, completed, setTasks, tasks, setError); - } + void handleLogout() { + authEffects.setToken(null); + authEffects.setUser(null); + authEffects.setView('login'); + } - void handleDelete(String id) { - _deleteTask(token, id, setTasks, tasks, setError); - } + void handleToggle(String id, bool completed) { + _toggleTask(token, id, completed, tasksState, errorState, fetchFn); + } - void handleAddTask() { - final title = newTaskTitle.trim(); - (title.isEmpty) - ? null - : _createTask(token, title, setTasks, tasks, setError, () { - setNewTaskTitle(''); - setShowAddForm(false); - }); - } + void handleDelete(String id) { + _deleteTask(token, id, tasksState, errorState, fetchFn); + } - final userName = user?['name'] as JSString?; + void handleAddTask() { + final title = newTaskTitle.trim(); + (title.isEmpty) + ? null + : _createTask(token, title, tasksState, errorState, fetchFn, () { + newTaskTitleState.set(''); + showAddFormState.set(false); + }); + } - return view( - style: AppStyles.container, - children: [ - _buildHeader(userName?.toDart ?? 'User', handleLogout), - loading - ? view( - style: {'flex': 1, 'justifyContent': 'center'}, - child: activityIndicator( - size: 'large', - color: AppColors.accentPrimary, - ), - ) - : (error?.isNotEmpty ?? false) - ? view( - style: {...AppStyles.errorMsg, 'margin': AppSpacing.xl}, - child: text(error ?? '', style: AppStyles.errorText), - ) - : _buildTaskContent( - tasks: tasks, - effects: (onToggle: handleToggle, onDelete: handleDelete), - showAddForm: showAddForm, - newTaskTitle: newTaskTitle, - setNewTaskTitle: setNewTaskTitle, - onAddTask: handleAddTask, - onCancelAdd: () => setShowAddForm(false), - ), - showAddForm - ? null - : touchableOpacity( - onPress: () => setShowAddForm(true), - style: AppStyles.fab, - child: text('+', style: AppStyles.fabText), - ), - ].whereType().toList(), - ); - }); + return view( + style: AppStyles.container, + children: [ + _buildHeader(getUserDisplayName(user), handleLogout), + loading + ? view( + style: {'flex': 1, 'justifyContent': 'center'}, + child: activityIndicator( + size: 'large', + color: AppColors.accentPrimary, + ), + ) + : (error?.isNotEmpty ?? false) + ? view( + style: {...AppStyles.errorMsg, 'margin': AppSpacing.xl}, + child: text(error ?? '', style: AppStyles.errorText), + ) + : _buildTaskContent( + tasks: tasks, + effects: (onToggle: handleToggle, onDelete: handleDelete), + showAddForm: showAddForm, + newTaskTitle: newTaskTitle, + setNewTaskTitle: newTaskTitleState.set, + onAddTask: handleAddTask, + onCancelAdd: () => showAddFormState.set(false), + ), + showAddForm + ? null + : touchableOpacity( + onPress: () => showAddFormState.set(true), + style: AppStyles.fab, + child: text('+', style: AppStyles.fabText), + ), + ].whereType().toList(), + ); +}); RNViewElement _buildHeader(String userName, void Function() onLogout) => view( - style: AppStyles.header, + style: AppStyles.header, + children: [ + text('TaskFlow', style: AppStyles.headerTitle), + view( + style: {'flexDirection': 'row', 'alignItems': 'center', 'gap': 16}, children: [ - text('TaskFlow', style: AppStyles.headerTitle), - view( - style: {'flexDirection': 'row', 'alignItems': 'center', 'gap': 16}, - children: [ - text('Hi, $userName', style: AppStyles.headerUserName), - touchableOpacity( - onPress: onLogout, - child: text('Logout', style: AppStyles.logoutText), - ), - ], + text('Hi, $userName', style: AppStyles.headerUserName), + touchableOpacity( + onPress: onLogout, + child: text('Logout', style: AppStyles.logoutText), ), ], - ); + ), + ], +); ReactElement _buildTaskContent({ - required List tasks, + required List tasks, required TaskEffects effects, required bool showAddForm, required String newTaskTitle, required void Function(String) setNewTaskTitle, required void Function() onAddTask, required void Function() onCancelAdd, -}) => - scrollView( - style: AppStyles.content, - children: [ - showAddForm ? _buildAddTaskForm(newTaskTitle, setNewTaskTitle, onAddTask, onCancelAdd) : null, - ...tasks.isEmpty - ? [_buildEmptyState()] - : tasks.map((task) => _buildTaskItem(task, effects)), - ].whereType().toList(), - ); +}) => scrollView( + style: AppStyles.content, + children: [ + showAddForm + ? _buildAddTaskForm( + newTaskTitle, + setNewTaskTitle, + onAddTask, + onCancelAdd, + ) + : null, + ...(tasks.isEmpty + ? [_buildEmptyState()] + : tasks.map((task) => _buildTaskItem(task, effects))), + ].whereType().toList(), +); RNViewElement _buildAddTaskForm( String value, void Function(String) onChangeText, void Function() onSubmit, void Function() onCancel, -) => - view( - style: AppStyles.addTaskInline, - children: [ - textInput( - value: value, - placeholder: 'What needs to be done?', - onChangeText: onChangeText, - style: AppStyles.addTaskInput, - props: {'placeholderTextColor': AppColors.textMuted, 'autoFocus': true}, - ), - touchableOpacity( - onPress: onSubmit, - style: AppStyles.addTaskBtn, - child: text('Add', style: AppStyles.addTaskBtnText), - ), - touchableOpacity( - onPress: onCancel, - style: AppStyles.cancelBtn, - child: text('Cancel', style: AppStyles.cancelBtnText), - ), - ], - ); +) => view( + style: AppStyles.addTaskInline, + children: [ + textInput( + value: value, + placeholder: 'What needs to be done?', + onChangeText: onChangeText, + style: AppStyles.addTaskInput, + props: {'placeholderTextColor': AppColors.textMuted, 'autoFocus': true}, + ), + touchableOpacity( + onPress: onSubmit, + style: AppStyles.addTaskBtn, + child: text('Add', style: AppStyles.addTaskBtnText), + ), + touchableOpacity( + onPress: onCancel, + style: AppStyles.cancelBtn, + child: text('Cancel', style: AppStyles.cancelBtnText), + ), + ], +); RNViewElement _buildEmptyState() => view( - style: AppStyles.emptyState, - children: [ - text('No tasks yet', style: AppStyles.emptyText), - ], - ); + style: AppStyles.emptyState, + children: [text('No tasks yet', style: AppStyles.emptyText)], +); -RNViewElement _buildTaskItem(JSAny? task, TaskEffects effects) { - final taskObj = task as JSObject?; - final id = (taskObj?['id'] as JSString?)?.toDart ?? ''; - final title = (taskObj?['title'] as JSString?)?.toDart ?? ''; - final completed = (taskObj?['completed'] as JSBoolean?)?.toDart ?? false; - - return view( - style: AppStyles.taskItem, - props: {'key': id}, - children: [ - touchableOpacity( - onPress: () => effects.onToggle(id, !completed), - child: view( - style: completed - ? AppStyles.checkboxChecked - : AppStyles.checkboxUnchecked, - child: completed ? text('✓', style: AppStyles.checkIcon) : null, - ), +RNViewElement _buildTaskItem(JSTask task, TaskEffects effects) => view( + style: AppStyles.taskItem, + props: {'key': task.id}, + children: [ + touchableOpacity( + onPress: () => effects.onToggle(task.id, !task.completed), + child: view( + style: task.completed + ? AppStyles.checkboxChecked + : AppStyles.checkboxUnchecked, + child: task.completed ? text('✓', style: AppStyles.checkIcon) : null, ), - view( - style: {'flex': 1}, - child: touchableOpacity( - onPress: () => effects.onToggle(id, !completed), - child: text( - title, - style: - completed ? AppStyles.taskTitleCompleted : AppStyles.taskTitle, - ), + ), + view( + style: {'flex': 1}, + child: touchableOpacity( + onPress: () => effects.onToggle(task.id, !task.completed), + child: text( + task.title, + style: task.completed + ? AppStyles.taskTitleCompleted + : AppStyles.taskTitle, ), ), - touchableOpacity( - onPress: () => effects.onDelete(id), - style: AppStyles.deleteBtn, - child: text('×', style: AppStyles.deleteBtnText), - ), - ], - ); -} + ), + touchableOpacity( + onPress: () => effects.onDelete(task.id), + style: AppStyles.deleteBtn, + child: text('×', style: AppStyles.deleteBtnText), + ), + ], +); void _loadTasks( String token, - JSFunction setTasks, - SetLoading setLoading, - SetError setError, + StateHookJSArray tasksState, + StateHook loadingState, + StateHook errorState, + Fetch? fetchFn, ) { - fetchTasks(token: token, apiUrl: apiUrl).then((result) { - result.match( - onSuccess: (tasks) { - setTasks.callAsFunction(null, tasks); - setError(null); - }, - onError: (message) => setError(message), - ); - }).catchError((Object e) { - setError(e.toString()); - }).whenComplete(() { - setLoading(false); - }); + final doFetch = fetchFn ?? fetchJson; + doFetch('$apiUrl/tasks', token: token) + .then((result) { + result.match( + onSuccess: (response) { + final data = response['data']; + switch (data) { + case final JSArray arr: + tasksState.set(_jsArrayToTasks(arr)); + case _: + tasksState.set([]); + } + errorState.set(null); + }, + onError: (message) => errorState.set(message), + ); + }) + .catchError((Object e) { + errorState.set(e.toString()); + }) + .whenComplete(() { + loadingState.set(false); + }); +} + +List _jsArrayToTasks(JSArray arr) { + final result = []; + for (var i = 0; i < arr.length; i++) { + switch (arr[i]) { + case final JSObject obj: + result.add(JSTask.fromJS(obj)); + case null: + break; + } + } + return result; } void _toggleTask( String token, String id, bool completed, - JSFunction setTasks, - List tasks, - SetError setError, + StateHookJSArray tasksState, + StateHook errorState, + Fetch? fetchFn, ) { - fetchJson( - '$apiUrl/tasks/$id', - method: 'PUT', - token: token, - body: {'completed': completed}, - ).then((result) { - result.match( - onSuccess: (_) { - final updated = tasks.map((t) { - final obj = t as JSObject?; - final taskId = (obj?['id'] as JSString?)?.toDart; - return (taskId == id) ? _updateTaskCompleted(obj, completed) : t; - }).toList(); - setTasks.callAsFunction(null, updated.toJS); - setError(null); - }, - onError: (message) => setError(message), - ); - }).catchError((Object e) { - setError(e.toString()); - }); -} - -JSObject _updateTaskCompleted(JSObject? task, bool completed) { - final newTask = JSObject(); - final keys = getObjectKeys(task ?? JSObject()); - for (final key in keys) { - newTask.setProperty(key.toJS, task?[key]); - } - newTask.setProperty('completed'.toJS, completed.toJS); - return newTask; + final doFetch = fetchFn ?? fetchJson; + doFetch( + '$apiUrl/tasks/$id', + method: 'PUT', + token: token, + body: {'completed': completed}, + ) + .then((result) { + result.match( + onSuccess: (_) { + tasksState.setWithUpdater((tasks) { + return tasks.map((t) { + return (t.id == id) ? t.withCompleted(completed) : t; + }).toList(); + }); + errorState.set(null); + }, + onError: (message) => errorState.set(message), + ); + }) + .catchError((Object e) { + errorState.set(e.toString()); + }); } void _deleteTask( String token, String id, - JSFunction setTasks, - List tasks, - SetError setError, + StateHookJSArray tasksState, + StateHook errorState, + Fetch? fetchFn, ) { - fetchJson('$apiUrl/tasks/$id', method: 'DELETE', token: token).then((result) { - result.match( - onSuccess: (_) { - final filtered = tasks.where((t) { - final obj = t as JSObject?; - final taskId = (obj?['id'] as JSString?)?.toDart; - return taskId != id; - }).toList(); - setTasks.callAsFunction(null, filtered.toJS); - setError(null); - }, - onError: (message) => setError(message), - ); - }).catchError((Object e) { - setError(e.toString()); - }); + final doFetch = fetchFn ?? fetchJson; + doFetch('$apiUrl/tasks/$id', method: 'DELETE', token: token) + .then((result) { + result.match( + onSuccess: (_) { + tasksState.setWithUpdater((tasks) { + return tasks.where((t) => t.id != id).toList(); + }); + errorState.set(null); + }, + onError: (message) => errorState.set(message), + ); + }) + .catchError((Object e) { + errorState.set(e.toString()); + }); } void _createTask( String token, String title, - JSFunction setTasks, - List tasks, - SetError setError, + StateHookJSArray tasksState, + StateHook errorState, + Fetch? fetchFn, void Function() onSuccess, ) { - fetchJson( - '$apiUrl/tasks', - method: 'POST', - token: token, - body: {'title': title}, - ).then((result) { - result.match( - onSuccess: (data) { - final task = data as JSObject?; - (task != null) - ? setTasks.callAsFunction(null, [...tasks, task].toJS) - : null; - setError(null); - onSuccess(); - }, - onError: (message) => setError(message), - ); - }).catchError((Object e) { - setError(e.toString()); - }); + final doFetch = fetchFn ?? fetchJson; + doFetch('$apiUrl/tasks', method: 'POST', token: token, body: {'title': title}) + .then((result) { + result.match( + onSuccess: (response) { + final data = response['data']; + switch (data) { + case final JSObject obj: + final newTask = JSTask.fromJS(obj); + // Deduplicate: only add if not already present (WS might have added it) + tasksState.setWithUpdater( + (tasks) => addTaskIfNotExists(tasks, newTask), + ); + case _: + break; + } + errorState.set(null); + onSuccess(); + }, + onError: (message) => errorState.set(message), + ); + }) + .catchError((Object e) { + errorState.set(e.toString()); + }); } /// Handle incoming WebSocket task events using functional updater void _handleTaskEvent( String? type, - JSObject task, - JSFunction setTasks, + JSTask task, + StateHookJSArray tasksState, ) { - final taskId = (task['id'] as JSString?)?.toDart; - - // Use functional updater to get current state - final updater = ((JSAny? prevState) { - final tasks = prevState as JSArray?; - final current = (tasks?.toDart ?? []).cast(); - - return switch (type) { - 'task_created' => [...current, task].toJS, - 'task_updated' => current.map((t) { - final id = (t['id'] as JSString?)?.toDart; - return (id == taskId) ? task : t; - }).toList().toJS, - 'task_deleted' => current.where((t) { - final id = (t['id'] as JSString?)?.toDart; - return id != taskId; - }).toList().toJS, - _ => prevState, - }; - }).toJS; - - setTasks.callAsFunction(null, updater); + tasksState.setWithUpdater((current) => handleTaskEvent(type, task, current)); } diff --git a/examples/mobile/lib/types.dart b/examples/mobile/lib/types.dart index 0ee1851..9f2ebdf 100644 --- a/examples/mobile/lib/types.dart +++ b/examples/mobile/lib/types.dart @@ -1,9 +1,13 @@ import 'dart:js_interop'; import 'dart:js_interop_unsafe'; +// Re-export shared JS types +export 'package:shared/js_types/js_types.dart'; + /// API configuration - reads from global set by build preamble String get apiUrl => - (globalContext['__API_URL__'] as JSString?)?.toDart ?? 'http://localhost:3000'; + (globalContext['__API_URL__'] as JSString?)?.toDart ?? + 'http://localhost:3000'; /// WebSocket URL - derives from API URL (port 3001) String get wsUrl { @@ -39,25 +43,3 @@ typedef OnDeleteTask = void Function(String id); /// Task effects bundle typedef TaskEffects = ({OnToggleTask onToggle, OnDeleteTask onDelete}); - -/// Helper to wrap JSFunction setState calls -SetFormValue wrapSetState(JSFunction setState) => - (value) => setState.callAsFunction( - null, - switch (value) { - final String s => s.toJS, - final bool b => b.toJS, - final int i => i.toJS, - final double d => d.toJS, - null => null, - _ => value as JSAny, - }, - ); - -/// Helper to wrap JSFunction setState for nullable JSAny -void Function(JSAny?) wrapSetStateJSAny(JSFunction setState) => - (value) => setState.callAsFunction(null, value); - -/// Clear state helper -void Function() wrapClearState(JSFunction setState) => - () => setState.callAsFunction(); diff --git a/examples/mobile/lib/websocket.dart b/examples/mobile/lib/websocket.dart index 1e40000..520cb79 100644 --- a/examples/mobile/lib/websocket.dart +++ b/examples/mobile/lib/websocket.dart @@ -39,32 +39,31 @@ RNWebSocket? connectWebSocket({ required void Function(JSObject event) onTaskEvent, void Function()? onOpen, void Function()? onClose, -}) => - _createWebSocket('$wsUrl?token=$token') - ..onopen = ((JSAny _) { - onOpen?.call(); - }).toJS - ..onmessage = ((WSMessageEvent event) { - final data = event.data; - switch (data.isA()) { - case true: - final message = data.dartify() as String?; - switch (message) { - case final String m: - _handleMessage(m, onTaskEvent); - case null: - break; - } - case false: +}) => _createWebSocket('$wsUrl?token=$token') + ..onopen = ((JSAny _) { + onOpen?.call(); + }).toJS + ..onmessage = ((WSMessageEvent event) { + final data = event.data; + switch (data.isA()) { + case true: + final message = data.dartify() as String?; + switch (message) { + case final String m: + _handleMessage(m, onTaskEvent); + case null: break; } - }).toJS - ..onclose = ((JSAny _) { - onClose?.call(); - }).toJS - ..onerror = ((JSAny _) { - // Error handling - close will be called after - }).toJS; + case false: + break; + } + }).toJS + ..onclose = ((JSAny _) { + onClose?.call(); + }).toJS + ..onerror = ((JSAny _) { + // Error handling - close will be called after + }).toJS; void _handleMessage(String message, void Function(JSObject) onTaskEvent) { final json = globalContext['JSON']! as JSObject; diff --git a/examples/mobile/pubspec.lock b/examples/mobile/pubspec.lock index 8466c7f..b1c65f2 100644 --- a/examples/mobile/pubspec.lock +++ b/examples/mobile/pubspec.lock @@ -1,6 +1,38 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "5b7468c326d2f8a4f630056404ca0d291ade42918f4a3c6233618e724f39da8e" + url: "https://pub.dev" + source: hosted + version: "92.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "70e4b1ef8003c64793a9e268a551a82869a8a96f39deb73dea28084b0e8bf75e" + url: "https://pub.dev" + source: hosted + version: "9.0.0" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" austerity: dependency: transitive description: @@ -9,6 +41,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" dart_node_core: dependency: transitive description: @@ -30,6 +110,54 @@ packages: relative: true source: path version: "0.2.0-beta" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" lints: dependency: "direct dev" description: @@ -38,6 +166,38 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.dev" + source: hosted + version: "0.12.18" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" nadz: dependency: "direct main" description: @@ -54,6 +214,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" shared: dependency: "direct main" description: @@ -61,5 +253,181 @@ packages: relative: true source: path version: "1.0.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: "77cc98ea27006c84e71a7356cf3daf9ddbde2d91d84f77dbfe64cf0e4d9611ae" + url: "https://pub.dev" + source: hosted + version: "1.28.0" + test_api: + dependency: transitive + description: + name: test_api + sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" + url: "https://pub.dev" + source: hosted + version: "0.7.8" + test_core: + dependency: transitive + description: + name: test_core + sha256: f1072617a6657e5fc09662e721307f7fb009b4ed89b19f47175d11d5254a62d4 + url: "https://pub.dev" + source: hosted + version: "0.6.14" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" + url: "https://pub.dev" + source: hosted + version: "1.1.4" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: dart: ">=3.10.0 <4.0.0" diff --git a/examples/mobile/pubspec.yaml b/examples/mobile/pubspec.yaml index 694694d..3102977 100644 --- a/examples/mobile/pubspec.yaml +++ b/examples/mobile/pubspec.yaml @@ -17,3 +17,4 @@ dependencies: dev_dependencies: lints: ^5.0.0 + test: ^1.25.0 diff --git a/examples/mobile/test/login_screen_test.dart b/examples/mobile/test/login_screen_test.dart new file mode 100644 index 0000000..3a1a361 --- /dev/null +++ b/examples/mobile/test/login_screen_test.dart @@ -0,0 +1,1588 @@ +/// UI interaction tests for the mobile app. +/// +/// Tests verify actual user interactions using the real lib/ components. +/// Run with: dart test -p chrome +@TestOn('js') +library; + +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +import 'package:dart_node_react/src/testing_library.dart'; +import 'package:mobile/app.dart' show MobileApp; +import 'package:mobile/websocket.dart'; +import 'package:nadz/nadz.dart'; +import 'package:shared/http/http_client.dart' show Fetch; +import 'package:test/test.dart'; + +import 'test_helpers.dart'; + +void main() { + setUp(setupMocks); + + // ===== LOGIN SCREEN TESTS ===== + + group('Login Screen', () { + test('renders login screen with Sign In text', () { + final result = render(MobileApp()); + + expect(result.container.textContent, contains('Sign In')); + expect(result.container.textContent, contains('Email')); + expect(result.container.textContent, contains('Password')); + + result.unmount(); + }); + + test('complete login flow - success', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'test-token-123', + 'user': {'name': 'Test User', 'email': 'test@example.com'}, + }, + }, + '/tasks': {'success': true, 'data': >[]}, + }); + + final result = render(MobileApp(fetchFn: mockFetch)); + + expect(result.container.textContent, contains('Sign In')); + + final inputs = result.container.querySelectorAll('input'); + expect(inputs.length, greaterThanOrEqualTo(2)); + + await userType(inputs[0], 'test@example.com'); + await userType(inputs[1], 'password123'); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.first); + + await waitForText(result, 'TaskFlow'); + + result.unmount(); + }); + + test('complete login flow - error', () async { + final mockFetch = createMockFetch({ + '/auth/login': {'success': false, 'error': 'Invalid credentials'}, + }); + + final result = render(MobileApp(fetchFn: mockFetch)); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'bad@email.com'); + await userType(inputs[1], 'wrong'); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.first); + + await waitForText(result, 'Invalid credentials'); + expect(result.container.textContent, contains('Sign In')); + + result.unmount(); + }); + + test('login with fetch exception shows error', () async { + final throwingFetch = createThrowingFetch(); + + final result = render(MobileApp(fetchFn: throwingFetch)); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'x@y.com'); + await userType(inputs[1], 'pass'); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.first); + + await waitForText(result, 'Network error'); + + result.unmount(); + }); + + test('login with no token in response', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'user': {'name': 'No Token User'}, + }, + }, + }); + + final result = render(MobileApp(fetchFn: mockFetch)); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'x@y.com'); + await userType(inputs[1], 'pass'); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.first); + + await waitForText(result, 'No token'); + + result.unmount(); + }); + + test('login with null data in response', () async { + final mockFetch = createMockFetch({ + '/auth/login': {'success': true, 'data': null}, + }); + + final result = render(MobileApp(fetchFn: mockFetch)); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'x@y.com'); + await userType(inputs[1], 'pass'); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.first); + + await waitForText(result, 'Login failed'); + + result.unmount(); + }); + + test('switch between login and register views', () { + final result = render(MobileApp()); + + expect(result.container.textContent, contains('Sign In')); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.last); + + expect(result.container.textContent, contains('Create Account')); + expect(result.container.textContent, contains('Name')); + + final registerButtons = result.container.querySelectorAll('button'); + fireClick(registerButtons.last); + + expect(result.container.textContent, contains('Sign In')); + + result.unmount(); + }); + }); + + // ===== REGISTER SCREEN TESTS ===== + + group('Register Screen', () { + test('register flow - success', () async { + final mockFetch = createMockFetch({ + '/auth/register': { + 'success': true, + 'data': { + 'token': 'new-token', + 'user': {'name': 'New User'}, + }, + }, + '/tasks': {'success': true, 'data': >[]}, + }); + + final result = render(MobileApp(fetchFn: mockFetch)); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.last); + + await waitForText(result, 'Create Account'); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'New User'); + await userType(inputs[1], 'new@user.com'); + await userType(inputs[2], 'password'); + + final registerButtons = result.container.querySelectorAll('button'); + fireClick(registerButtons.first); + + await waitForText(result, 'TaskFlow'); + + result.unmount(); + }); + + test('register with server error', () async { + final mockFetch = createMockFetch({ + '/auth/register': {'success': false, 'error': 'Email already exists'}, + }); + + final result = render(MobileApp(fetchFn: mockFetch)); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.last); + + await waitForText(result, 'Create Account'); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'Name'); + await userType(inputs[1], 'existing@email.com'); + await userType(inputs[2], 'pass'); + + final registerButtons = result.container.querySelectorAll('button'); + fireClick(registerButtons.first); + + await waitForText(result, 'Email already exists'); + + result.unmount(); + }); + + test('register with fetch exception', () async { + final throwingFetch = createThrowingFetch(); + + final result = render(MobileApp(fetchFn: throwingFetch)); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.last); + + await waitForText(result, 'Create Account'); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'Name'); + await userType(inputs[1], 'a@b.com'); + await userType(inputs[2], 'pass'); + + final registerButtons = result.container.querySelectorAll('button'); + fireClick(registerButtons.first); + + await waitForText(result, 'Network error'); + + result.unmount(); + }); + + test('register with no token in response', () async { + final mockFetch = createMockFetch({ + '/auth/register': { + 'success': true, + 'data': { + 'user': {'name': 'No Token'}, + }, + }, + }); + + final result = render(MobileApp(fetchFn: mockFetch)); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.last); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'Name'); + await userType(inputs[1], 'a@b.com'); + await userType(inputs[2], 'pass'); + + final registerButtons = result.container.querySelectorAll('button'); + fireClick(registerButtons.first); + + // Should navigate to tasks (null token is set) + await waitForText(result, 'TaskFlow'); + + result.unmount(); + }); + + test('register with null data in response', () async { + final mockFetch = createMockFetch({ + '/auth/register': {'success': true, 'data': null}, + }); + + final result = render(MobileApp(fetchFn: mockFetch)); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.last); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'Name'); + await userType(inputs[1], 'a@b.com'); + await userType(inputs[2], 'pass'); + + final registerButtons = result.container.querySelectorAll('button'); + fireClick(registerButtons.first); + + // Should navigate to tasks (null token/user) + await waitForText(result, 'TaskFlow'); + + result.unmount(); + }); + }); + + // ===== TASK LIST SCREEN TESTS ===== + + group('Task List Screen', () { + Future loginAndNavigateToTasks(Fetch mockFetch) async { + final result = render(MobileApp(fetchFn: mockFetch)); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'test@example.com'); + await userType(inputs[1], 'password'); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.first); + + await waitForText(result, 'TaskFlow'); + return result; + } + + test('shows tasks after successful login', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': { + 'success': true, + 'data': [ + {'id': '1', 'title': 'Task One', 'completed': false}, + {'id': '2', 'title': 'Task Two', 'completed': true}, + ], + }, + }); + + final result = await loginAndNavigateToTasks(mockFetch); + + await waitForText(result, 'Task One'); + expect(result.container.textContent, contains('Task Two')); + + result.unmount(); + }); + + test('shows empty state when no tasks', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': {'success': true, 'data': >[]}, + }); + + final result = await loginAndNavigateToTasks(mockFetch); + + await waitForText(result, 'No tasks yet'); + + result.unmount(); + }); + + test('shows user name in header', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Charlie'}, + }, + }, + '/tasks': {'success': true, 'data': >[]}, + }); + + final result = await loginAndNavigateToTasks(mockFetch); + + await waitForText(result, 'Hi, Charlie'); + + result.unmount(); + }); + + test('shows "User" when name is null', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'email': 'no-name@test.com'}, + }, + }, + '/tasks': {'success': true, 'data': >[]}, + }); + + final result = await loginAndNavigateToTasks(mockFetch); + + await waitForText(result, 'Hi, User'); + + result.unmount(); + }); + + test('shows error message when task load fails', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Test'}, + }, + }, + '/tasks': {'success': false, 'error': 'Failed to load tasks'}, + }); + + final result = await loginAndNavigateToTasks(mockFetch); + + await waitForText(result, 'Failed to load tasks'); + + result.unmount(); + }); + + test('shows error when task load throws exception', () async { + var callCount = 0; + Future> customFetch( + String url, { + String method = 'GET', + String? token, + Map? body, + }) async { + if (url.contains('/auth/login')) { + return Success( + createJSObject({ + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Test'}, + }, + }), + ); + } + if (url.contains('/tasks')) { + callCount++; + throw Exception('Network error'); + } + throw StateError('No mock for $method $url'); + } + + final result = render(MobileApp(fetchFn: customFetch)); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'test@example.com'); + await userType(inputs[1], 'password'); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.first); + + await waitForText(result, 'TaskFlow'); + await waitForText(result, 'Exception: Network error'); + + expect(callCount, greaterThan(0)); + + result.unmount(); + }); + + test('logout clears state and returns to login', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': {'success': true, 'data': >[]}, + }); + + final result = await loginAndNavigateToTasks(mockFetch); + + await waitForText(result, 'Logout'); + + // Find and click Logout button + final buttons = result.container.querySelectorAll('button'); + // Logout is the button containing "Logout" text + for (final btn in buttons) { + if (btn.textContent.contains('Logout')) { + fireClick(btn); + break; + } + } + + await waitForText(result, 'Sign In'); + expect(result.container.textContent, isNot(contains('TaskFlow'))); + + result.unmount(); + }); + + test('toggle task completion - success', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': { + 'success': true, + 'data': [ + {'id': '1', 'title': 'Toggle Me', 'completed': false}, + ], + }, + 'PUT /tasks/1': {'success': true, 'data': {}}, + }); + + final result = await loginAndNavigateToTasks(mockFetch); + + await waitForText(result, 'Toggle Me'); + + // Find buttons - the checkbox is one of the first buttons after TaskFlow + // Click on the task title text to toggle (it's a touchable) + final allButtons = result.container.querySelectorAll('button'); + // Skip header buttons, find one that triggers toggle + for (final btn in allButtons) { + if (btn.textContent == 'Toggle Me') { + fireClick(btn); + break; + } + } + + // Give time for state update + await Future.delayed(const Duration(milliseconds: 200)); + + result.unmount(); + }); + + test('toggle task completion - error', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': { + 'success': true, + 'data': [ + {'id': '1', 'title': 'Toggle Error', 'completed': false}, + ], + }, + 'PUT /tasks/1': {'success': false, 'error': 'Toggle failed'}, + }); + + final result = await loginAndNavigateToTasks(mockFetch); + + await waitForText(result, 'Toggle Error'); + + final allButtons = result.container.querySelectorAll('button'); + for (final btn in allButtons) { + if (btn.textContent == 'Toggle Error') { + fireClick(btn); + break; + } + } + + await waitForText(result, 'Toggle failed'); + + result.unmount(); + }); + + test('toggle task throws exception', () async { + var toggleCalled = false; + Future> customFetch( + String url, { + String method = 'GET', + String? token, + Map? body, + }) async { + if (url.contains('/auth/login')) { + return Success( + createJSObject({ + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Test'}, + }, + }), + ); + } + if (url.contains('/tasks') && method == 'GET') { + return Success( + createJSObject({ + 'success': true, + 'data': [ + {'id': '1', 'title': 'Toggle Exception', 'completed': false}, + ], + }), + ); + } + if (url.contains('/tasks/1') && method == 'PUT') { + toggleCalled = true; + throw Exception('Toggle network error'); + } + throw StateError('No mock for $method $url'); + } + + final result = render(MobileApp(fetchFn: customFetch)); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'test@example.com'); + await userType(inputs[1], 'password'); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.first); + + await waitForText(result, 'Toggle Exception'); + + final allButtons = result.container.querySelectorAll('button'); + for (final btn in allButtons) { + if (btn.textContent == 'Toggle Exception') { + fireClick(btn); + break; + } + } + + await waitForText(result, 'Exception: Toggle network error'); + expect(toggleCalled, isTrue); + + result.unmount(); + }); + + test('delete task - success', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': { + 'success': true, + 'data': [ + {'id': '1', 'title': 'Delete Me', 'completed': false}, + ], + }, + 'DELETE /tasks/1': {'success': true, 'data': {}}, + }); + + final result = await loginAndNavigateToTasks(mockFetch); + + await waitForText(result, 'Delete Me'); + + // Find delete button (the × button) + final allButtons = result.container.querySelectorAll('button'); + for (final btn in allButtons) { + if (btn.textContent.contains('×')) { + fireClick(btn); + break; + } + } + + // Wait for task to be removed + await Future.delayed(const Duration(milliseconds: 200)); + await waitForText(result, 'No tasks yet'); + + result.unmount(); + }); + + test('delete task - error shows error message', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': { + 'success': true, + 'data': [ + {'id': '1', 'title': 'Task To Delete', 'completed': false}, + ], + }, + 'DELETE /tasks/1': {'success': false, 'error': 'Delete failed'}, + }); + + final result = await loginAndNavigateToTasks(mockFetch); + + await waitForText(result, 'Task To Delete'); + + final allButtons = result.container.querySelectorAll('button'); + for (final btn in allButtons) { + if (btn.textContent.contains('×')) { + fireClick(btn); + break; + } + } + + // Error message should appear + await waitForText(result, 'Delete failed'); + + result.unmount(); + }); + + test('delete task throws exception', () async { + var deleteCalled = false; + Future> customFetch( + String url, { + String method = 'GET', + String? token, + Map? body, + }) async { + if (url.contains('/auth/login')) { + return Success( + createJSObject({ + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Test'}, + }, + }), + ); + } + if (url.contains('/tasks') && method == 'GET') { + return Success( + createJSObject({ + 'success': true, + 'data': [ + {'id': '1', 'title': 'Delete Exception', 'completed': false}, + ], + }), + ); + } + if (url.contains('/tasks/1') && method == 'DELETE') { + deleteCalled = true; + throw Exception('Delete network error'); + } + throw StateError('No mock for $method $url'); + } + + final result = render(MobileApp(fetchFn: customFetch)); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'test@example.com'); + await userType(inputs[1], 'password'); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.first); + + await waitForText(result, 'Delete Exception'); + + final allButtons = result.container.querySelectorAll('button'); + for (final btn in allButtons) { + if (btn.textContent.contains('×')) { + fireClick(btn); + break; + } + } + + await waitForText(result, 'Exception: Delete network error'); + expect(deleteCalled, isTrue); + + result.unmount(); + }); + + test('FAB shows add form when clicked', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': {'success': true, 'data': >[]}, + }); + + final result = await loginAndNavigateToTasks(mockFetch); + + await waitForText(result, 'No tasks yet'); + + // Find and click the FAB (+ button) + final allButtons = result.container.querySelectorAll('button'); + for (final btn in allButtons) { + if (btn.textContent == '+') { + fireClick(btn); + break; + } + } + + // Wait for form to appear (Add button visible means form is showing) + await waitForText(result, 'Cancel'); + expect(result.container.textContent, contains('Add')); + + result.unmount(); + }); + + test('add task form - cancel hides form', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': {'success': true, 'data': >[]}, + }); + + final result = await loginAndNavigateToTasks(mockFetch); + + // Click FAB + final allButtons = result.container.querySelectorAll('button'); + for (final btn in allButtons) { + if (btn.textContent == '+') { + fireClick(btn); + break; + } + } + + await waitForText(result, 'Cancel'); + + // Click Cancel + final formButtons = result.container.querySelectorAll('button'); + for (final btn in formButtons) { + if (btn.textContent == 'Cancel') { + fireClick(btn); + break; + } + } + + // Form should be hidden, FAB should be back + await Future.delayed(const Duration(milliseconds: 100)); + expect(result.container.textContent, contains('+')); + + result.unmount(); + }); + + test('add new task - success', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': {'success': true, 'data': >[]}, + 'POST /tasks': { + 'success': true, + 'data': { + 'id': 'new-1', + 'title': 'Brand New Task', + 'completed': false, + }, + }, + }); + + final result = await loginAndNavigateToTasks(mockFetch); + + // Click FAB + final allButtons = result.container.querySelectorAll('button'); + for (final btn in allButtons) { + if (btn.textContent == '+') { + fireClick(btn); + break; + } + } + + await waitForText(result, 'Cancel'); + + // Type task title + final inputs = result.container.querySelectorAll('input'); + await userType(inputs.first, 'Brand New Task'); + + // Click Add + final formButtons = result.container.querySelectorAll('button'); + for (final btn in formButtons) { + if (btn.textContent == 'Add') { + fireClick(btn); + break; + } + } + + await waitForText(result, 'Brand New Task'); + // Form should be hidden + expect(result.container.textContent, isNot(contains('Cancel'))); + + result.unmount(); + }); + + test('add new task - empty title ignored', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': {'success': true, 'data': >[]}, + }); + + final result = await loginAndNavigateToTasks(mockFetch); + + // Click FAB + final allButtons = result.container.querySelectorAll('button'); + for (final btn in allButtons) { + if (btn.textContent == '+') { + fireClick(btn); + break; + } + } + + await waitForText(result, 'Cancel'); + + // Don't type anything, just click Add + final formButtons = result.container.querySelectorAll('button'); + for (final btn in formButtons) { + if (btn.textContent == 'Add') { + fireClick(btn); + break; + } + } + + // Form should still be visible (nothing happened) + await Future.delayed(const Duration(milliseconds: 100)); + expect(result.container.textContent, contains('Cancel')); + + result.unmount(); + }); + + test('add task - error', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': {'success': true, 'data': >[]}, + 'POST /tasks': {'success': false, 'error': 'Create failed'}, + }); + + final result = await loginAndNavigateToTasks(mockFetch); + + final allButtons = result.container.querySelectorAll('button'); + for (final btn in allButtons) { + if (btn.textContent == '+') { + fireClick(btn); + break; + } + } + + await waitForText(result, 'Cancel'); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs.first, 'Failing Task'); + + final formButtons = result.container.querySelectorAll('button'); + for (final btn in formButtons) { + if (btn.textContent == 'Add') { + fireClick(btn); + break; + } + } + + await waitForText(result, 'Create failed'); + + result.unmount(); + }); + + test('add task throws exception', () async { + var createCalled = false; + Future> customFetch( + String url, { + String method = 'GET', + String? token, + Map? body, + }) async { + if (url.contains('/auth/login')) { + return Success( + createJSObject({ + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Test'}, + }, + }), + ); + } + if (url.contains('/tasks') && method == 'GET') { + return Success( + createJSObject({'success': true, 'data': >[]}), + ); + } + if (url.contains('/tasks') && method == 'POST') { + createCalled = true; + throw Exception('Create network error'); + } + throw StateError('No mock for $method $url'); + } + + final result = render(MobileApp(fetchFn: customFetch)); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'test@example.com'); + await userType(inputs[1], 'password'); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.first); + + await waitForText(result, 'TaskFlow'); + + final allButtons = result.container.querySelectorAll('button'); + for (final btn in allButtons) { + if (btn.textContent == '+') { + fireClick(btn); + break; + } + } + + await waitForText(result, 'Cancel'); + + final taskInputs = result.container.querySelectorAll('input'); + await userType(taskInputs.first, 'Exception Task'); + + final formButtons = result.container.querySelectorAll('button'); + for (final btn in formButtons) { + if (btn.textContent == 'Add') { + fireClick(btn); + break; + } + } + + await waitForText(result, 'Exception: Create network error'); + expect(createCalled, isTrue); + + result.unmount(); + }); + }); + + // ===== DUPLICATE TASK BUG TESTS ===== + + group('Duplicate Task Prevention', () { + test('add task - no duplicates when WS arrives after HTTP', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': {'success': true, 'data': >[]}, + 'POST /tasks': { + 'success': true, + 'data': {'id': '99', 'title': 'My New Task', 'completed': false}, + }, + }); + + final result = render(MobileApp(fetchFn: mockFetch)); + + // Login + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'a@b.com'); + await userType(inputs[1], 'pass'); + fireClick(result.container.querySelectorAll('button').first); + + await waitForText(result, 'No tasks yet'); + + // Click FAB to add task + final allButtons = result.container.querySelectorAll('button'); + for (final btn in allButtons) { + if (btn.textContent == '+') { + fireClick(btn); + break; + } + } + + await waitForText(result, 'Cancel'); + + // Type and submit + final taskInputs = result.container.querySelectorAll('input'); + await userType(taskInputs.first, 'My New Task'); + + final formButtons = result.container.querySelectorAll('button'); + for (final btn in formButtons) { + if (btn.textContent == 'Add') { + fireClick(btn); + break; + } + } + + await waitForText(result, 'My New Task'); + + // Simulate WebSocket also sending task_created (what the server does!) + simulateWsMessage( + '{"type":"task_created","data":' + '{"id":"99","title":"My New Task","completed":false}}', + ); + + // Give React time to process the WebSocket event + await Future.delayed(const Duration(milliseconds: 100)); + + // CRITICAL: Count task items - should be exactly 1! + final taskButtons = result.container.querySelectorAll('button'); + var taskCount = 0; + for (final btn in taskButtons) { + if (btn.textContent == 'My New Task') { + taskCount++; + } + } + expect( + taskCount, + 1, + reason: 'Should have exactly 1 task, not duplicates', + ); + + result.unmount(); + }); + + test( + 'add task - WS arrives BEFORE HTTP response (race condition)', + () async { + // This tests the REAL bug: WebSocket is faster than HTTP response! + // Server broadcasts task_created immediately, HTTP response is slower. + + Future> racingFetch( + String url, { + String method = 'GET', + String? token, + Map? body, + }) async { + if (url.contains('/auth/login')) { + return Success( + createJSObject({ + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }), + ); + } + if (url.contains('/tasks') && method == 'GET') { + return Success( + createJSObject({ + 'success': true, + 'data': >[], + }), + ); + } + if (url.contains('/tasks') && method == 'POST') { + // SIMULATE RACE: WebSocket arrives BEFORE HTTP response! + simulateWsMessage( + '{"type":"task_created","data":' + '{"id":"race-1","title":"Race Task","completed":false}}', + ); + // Small delay, then HTTP returns + await Future.delayed(const Duration(milliseconds: 50)); + return Success( + createJSObject({ + 'success': true, + 'data': { + 'id': 'race-1', + 'title': 'Race Task', + 'completed': false, + }, + }), + ); + } + throw StateError('No mock for $method $url'); + } + + final result = render(MobileApp(fetchFn: racingFetch)); + + // Login + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'a@b.com'); + await userType(inputs[1], 'pass'); + fireClick(result.container.querySelectorAll('button').first); + + await waitForText(result, 'No tasks yet'); + + // Click FAB + final allButtons = result.container.querySelectorAll('button'); + for (final btn in allButtons) { + if (btn.textContent == '+') { + fireClick(btn); + break; + } + } + + await waitForText(result, 'Cancel'); + + // Type and submit - WS will arrive before HTTP! + final taskInputs = result.container.querySelectorAll('input'); + await userType(taskInputs.first, 'Race Task'); + + final formButtons = result.container.querySelectorAll('button'); + for (final btn in formButtons) { + if (btn.textContent == 'Add') { + fireClick(btn); + break; + } + } + + // Wait for task to appear + await waitForText(result, 'Race Task'); + await Future.delayed(const Duration(milliseconds: 200)); + + // CRITICAL: Count task items - must be exactly 1! + final taskButtons = result.container.querySelectorAll('button'); + var taskCount = 0; + for (final btn in taskButtons) { + if (btn.textContent == 'Race Task') { + taskCount++; + } + } + expect( + taskCount, + 1, + reason: 'WS arrived first, HTTP should NOT add duplicate!', + ); + + result.unmount(); + }, + ); + }); + + // ===== WEBSOCKET TESTS ===== + + group('WebSocket Events', () { + test( + 'WS task_created DURING initial load does NOT create duplicate', + () async { + // BUG: WebSocket task_created arrives DURING initial HTTP load, + // BEFORE HTTP response. Then HTTP arrives and REPLACES the list, + // but HTTP data + WS data = duplicate! + // + // Timeline: + // 1. Login succeeds + // 2. Component starts GET /tasks (async) + // 3. WebSocket connects, server sends task_created event + // 4. WS handler adds task to state (currently empty) + // 5. HTTP /tasks responds with SAME task + // 6. _loadTasks does tasksState.set() - REPLACES state! + // + // Result: WS added task + HTTP replaced with same task = OK normally + // BUT if we don't deduplicate in set(), we can get race issues. + // + // Actually the REAL bug: WS arrives AFTER HTTP completes, but for + // a task that was ALREADY in the HTTP response. Let's test that. + + Future> racingFetch( + String url, { + String method = 'GET', + String? token, + Map? body, + }) async { + if (url.contains('/auth/login')) { + return Success( + createJSObject({ + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }), + ); + } + if (url.contains('/tasks') && method == 'GET') { + // HTTP returns task, but ALSO WS will send same task later + return Success( + createJSObject({ + 'success': true, + 'data': [ + { + 'id': 'task_47', + 'title': 'Existing Task', + 'completed': false, + }, + ], + }), + ); + } + throw StateError('No mock for $method $url'); + } + + final result = render(MobileApp(fetchFn: racingFetch)); + + // Login + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'a@b.com'); + await userType(inputs[1], 'pass'); + fireClick(result.container.querySelectorAll('button').first); + + // Wait for task list to load + await waitForText(result, 'Existing Task'); + + // WS sends task_created for same task that's already loaded! + // This simulates server broadcasting to all clients + simulateWsMessage( + '{"type":"task_created","data":' + '{"id":"task_47","title":"Existing Task","completed":false}}', + ); + + await Future.delayed(const Duration(milliseconds: 100)); + + // Count occurrences - must be exactly 1! + final textContent = result.container.textContent; + final matches = 'Existing Task'.allMatches(textContent).length; + expect( + matches, + 1, + reason: 'WS task_created for existing task should NOT duplicate!', + ); + + result.unmount(); + }, + ); + + test('task_created event adds task to list', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': {'success': true, 'data': >[]}, + }); + + final result = render(MobileApp(fetchFn: mockFetch)); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'test@example.com'); + await userType(inputs[1], 'password'); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.first); + + await waitForText(result, 'No tasks yet'); + + // Simulate websocket task_created + simulateWsMessage( + '{"type":"task_created","data":' + '{"id":"ws-1","title":"WebSocket Task","completed":false}}', + ); + + await waitForText(result, 'WebSocket Task'); + + result.unmount(); + }); + + test('task_updated event updates task in list', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': { + 'success': true, + 'data': [ + {'id': '1', 'title': 'Original Title', 'completed': false}, + ], + }, + }); + + final result = render(MobileApp(fetchFn: mockFetch)); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'test@example.com'); + await userType(inputs[1], 'password'); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.first); + + await waitForText(result, 'Original Title'); + + // Simulate websocket task_updated + simulateWsMessage( + '{"type":"task_updated","data":' + '{"id":"1","title":"Updated Title","completed":true}}', + ); + + await waitForText(result, 'Updated Title'); + + result.unmount(); + }); + + test('task_deleted event removes task from list', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': { + 'success': true, + 'data': [ + {'id': '1', 'title': 'To Be Deleted', 'completed': false}, + ], + }, + }); + + final result = render(MobileApp(fetchFn: mockFetch)); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'test@example.com'); + await userType(inputs[1], 'password'); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.first); + + await waitForText(result, 'To Be Deleted'); + + // Simulate websocket task_deleted + simulateWsMessage( + '{"type":"task_deleted","data":' + '{"id":"1","title":"To Be Deleted","completed":false}}', + ); + + await waitForText(result, 'No tasks yet'); + + result.unmount(); + }); + + test('handles unknown event type gracefully', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': { + 'success': true, + 'data': [ + {'id': '1', 'title': 'Existing Task', 'completed': false}, + ], + }, + }); + + final result = render(MobileApp(fetchFn: mockFetch)); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'test@example.com'); + await userType(inputs[1], 'password'); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.first); + + await waitForText(result, 'Existing Task'); + + // Simulate websocket with unknown type + simulateWsMessage( + '{"type":"unknown_event","data":' + '{"id":"1","title":"Existing Task","completed":false}}', + ); + + // Task should still be there, unchanged + await Future.delayed(const Duration(milliseconds: 100)); + expect(result.container.textContent, contains('Existing Task')); + + result.unmount(); + }); + + test('handles event with null data gracefully', () async { + final mockFetch = createMockFetch({ + '/auth/login': { + 'success': true, + 'data': { + 'token': 'tok', + 'user': {'name': 'Alice'}, + }, + }, + '/tasks': { + 'success': true, + 'data': [ + {'id': '1', 'title': 'Still Here', 'completed': false}, + ], + }, + }); + + final result = render(MobileApp(fetchFn: mockFetch)); + + final inputs = result.container.querySelectorAll('input'); + await userType(inputs[0], 'test@example.com'); + await userType(inputs[1], 'password'); + + final buttons = result.container.querySelectorAll('button'); + fireClick(buttons.first); + + await waitForText(result, 'Still Here'); + + // Simulate websocket with null data + simulateWsMessage('{"type":"task_created","data":null}'); + + // Should not crash, task should still be there + await Future.delayed(const Duration(milliseconds: 100)); + expect(result.container.textContent, contains('Still Here')); + + result.unmount(); + }); + }); + + // ===== WEBSOCKET DIRECT TESTS ===== + + group('WebSocket API', () { + test('connectWebSocket creates websocket with handlers', () { + var openCalled = false; + var closeCalled = false; + final events = []; + + final ws = connectWebSocket( + token: 'test-token', + onTaskEvent: events.add, + onOpen: () => openCalled = true, + onClose: () => closeCalled = true, + ); + + expect(ws, isNotNull); + + // Trigger the handlers manually via the mock + final dummyEvent = JSObject(); + ws!.onopen?.callAsFunction(null, dummyEvent); + expect(openCalled, isTrue); + + ws.onclose?.callAsFunction(null, dummyEvent); + expect(closeCalled, isTrue); + + // Test message handler with valid JSON string + final messageEvent = JSObject(); + messageEvent['data'] = '{"type":"test"}'.toJS; + ws.onmessage?.callAsFunction(null, messageEvent); + expect(events.length, 1); + + // Test error handler (should not throw) + ws.onerror?.callAsFunction(null, dummyEvent); + + ws.close(); + }); + + test('connectWebSocket handles non-string message data', () { + final events = []; + + final ws = connectWebSocket(token: 'test-token', onTaskEvent: events.add); + + // Send non-string data (should be ignored) + final messageEvent = JSObject(); + messageEvent['data'] = 123.toJS; + ws!.onmessage?.callAsFunction(null, messageEvent); + + expect(events, isEmpty); + ws.close(); + }); + + test('connectWebSocket without optional callbacks', () { + final events = []; + + final ws = connectWebSocket( + token: 'test-token', + onTaskEvent: events.add, + // No onOpen, onClose + ); + + expect(ws, isNotNull); + + // Should not throw when calling handlers + final dummyEvent = JSObject(); + ws!.onopen?.callAsFunction(null, dummyEvent); + ws.onclose?.callAsFunction(null, dummyEvent); + + ws.close(); + }); + }); +} diff --git a/examples/mobile/test/test_helpers.dart b/examples/mobile/test/test_helpers.dart new file mode 100644 index 0000000..fae10c0 --- /dev/null +++ b/examples/mobile/test/test_helpers.dart @@ -0,0 +1,171 @@ +/// Test helpers and mock utilities for mobile app tests. +library; + +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +import 'package:dart_node_react/src/testing_library.dart'; +import 'package:mobile/types.dart'; +import 'package:nadz/nadz.dart'; +import 'package:shared/http/http_client.dart'; + +/// Create a mock AuthEffects for testing +AuthEffects createMockAuth({ + void Function(JSAny?)? onSetToken, + void Function(JSAny?)? onSetUser, + void Function(String)? onSetView, +}) => ( + setToken: onSetToken ?? (JSAny? _) {}, + setUser: onSetUser ?? (JSAny? _) {}, + setView: onSetView ?? (String _) {}, +); + +/// Create a JSObject from a Dart map +JSObject createJSObject(Map map) { + final json = globalContext['JSON']! as JSObject; + final parseFn = json['parse']! as JSFunction; + final jsonStr = _toJsonString(map); + return parseFn.callAsFunction(null, jsonStr.toJS)! as JSObject; +} + +String _toJsonString(Map map) { + final entries = map.entries.map((e) { + final value = e.value; + String valueStr; + if (value is String) { + valueStr = '"$value"'; + } else if (value is bool) { + valueStr = value.toString(); + } else if (value is int) { + valueStr = value.toString(); + } else if (value is double) { + valueStr = value.toString(); + } else if (value == null) { + valueStr = 'null'; + } else if (value is Map) { + valueStr = _toJsonString(value.cast()); + } else if (value is List) { + valueStr = _toJsonList(value.cast()); + } else { + valueStr = '"$value"'; + } + return '"${e.key}":$valueStr'; + }); + return '{${entries.join(',')}}'; +} + +String _toJsonList(List list) { + final items = list.map((item) { + if (item is String) return '"$item"'; + if (item is bool) return item.toString(); + if (item is int) return item.toString(); + if (item is double) return item.toString(); + if (item == null) return 'null'; + if (item is Map) return _toJsonString(item.cast()); + if (item is List) return _toJsonList(item.cast()); + return '"$item"'; + }); + return '[${items.join(',')}]'; +} + +/// Create a mock JSTask for testing +JSTask createMockTask({ + required String id, + required String title, + bool completed = false, +}) => JSTask.fromJS( + createJSObject({'id': id, 'title': title, 'completed': completed}), +); + +/// Create a mock JSUser for testing +JSUser createMockUser({ + required String name, + String email = 'test@example.com', +}) => JSUser.fromJS(createJSObject({'name': name, 'email': email})); + +// --- Fetch Mock Infrastructure --- + +/// Create a mock fetch function from URL pattern -> response map +/// +/// Keys can be: +/// - URL pattern: '/tasks' matches any method +/// - Method + URL: 'POST /tasks' matches only POST to /tasks +Fetch createMockFetch(Map> responses) => + (url, {method = 'GET', token, body}) async { + // First try method-specific match (e.g., 'POST /tasks') + final methodKey = '$method $url'.replaceAll(RegExp('https?://[^/]+'), ''); + for (final entry in responses.entries) { + if (entry.key.contains(' ') && methodKey.contains(entry.key)) { + return _buildResponse(entry.value); + } + } + // Fall back to URL-only match + for (final entry in responses.entries) { + if (entry.key.contains(' ')) continue; + if (url.contains(entry.key)) { + return _buildResponse(entry.value); + } + } + throw StateError('No mock for $method $url'); + }; + +Result _buildResponse(Map response) { + final success = response['success'] == true; + return success + ? Success(createJSObject(response)) + : Error(response['error']?.toString() ?? 'Request failed'); +} + +/// Create a fetch that throws to test error handling +Fetch createThrowingFetch() => + (url, {method = 'GET', token, body}) => + Future.error(Exception('Network error')); + +// --- WebSocket Mock --- + +JSObject? _lastMockWs; + +/// Setup mock WebSocket for testing +void mockWebSocket() { + globalContext['WebSocket'] = ((JSString url) { + final ws = JSObject(); + ws['close'] = (() {}).toJS; + ws['send'] = ((JSAny _) {}).toJS; + _lastMockWs = ws; + return ws; + }).toJS; +} + +/// Simulate a WebSocket message from the server +void simulateWsMessage(String json) { + final ws = _lastMockWs; + if (ws == null) return; + final onmessage = ws['onmessage']; + if (onmessage == null) return; + final event = JSObject(); + event['data'] = json.toJS; + (onmessage as JSFunction).callAsFunction(null, event); +} + +// --- Setup --- + +/// Setup all mocks for testing +void setupMocks() { + mockWebSocket(); +} + +// --- Wait Helpers --- + +/// Wait for text to appear in the rendered result +Future waitForText( + TestRenderResult result, + String text, { + int maxAttempts = 20, + Duration interval = const Duration(milliseconds: 100), +}) async { + for (var i = 0; i < maxAttempts; i++) { + if (result.container.textContent.contains(text)) return; + await Future.delayed(interval); + } + throw StateError('Text "$text" not found after $maxAttempts attempts'); +} diff --git a/examples/mobile/test/test_template.html b/examples/mobile/test/test_template.html new file mode 100644 index 0000000..883b523 --- /dev/null +++ b/examples/mobile/test/test_template.html @@ -0,0 +1,103 @@ + + + + + {{testName}} + + + + + {{testScript}} + + + diff --git a/examples/mobile/test/types_test.dart b/examples/mobile/test/types_test.dart new file mode 100644 index 0000000..9439513 --- /dev/null +++ b/examples/mobile/test/types_test.dart @@ -0,0 +1,166 @@ +/// Unit tests for mobile app types. +@TestOn('js') +library; + +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +import 'package:mobile/types.dart'; +import 'package:test/test.dart'; + +import 'test_helpers.dart'; + +void main() { + setUpAll(setupMocks); + + group('JSTask', () { + test('parses id from JSObject', () { + final task = createMockTask(id: 'task-123', title: 'Test'); + expect(task.id, equals('task-123')); + }); + + test('parses title from JSObject', () { + final task = createMockTask(id: '1', title: 'Buy groceries'); + expect(task.title, equals('Buy groceries')); + }); + + test('parses completed false by default', () { + final task = createMockTask(id: '1', title: 'Test'); + expect(task.completed, isFalse); + }); + + test('parses completed true', () { + final task = createMockTask(id: '1', title: 'Test', completed: true); + expect(task.completed, isTrue); + }); + + test('withCompleted creates new task with updated status', () { + final task = createMockTask(id: '1', title: 'Test', completed: false); + final updated = task.withCompleted(true); + + expect(updated.id, equals('1')); + expect(updated.title, equals('Test')); + expect(updated.completed, isTrue); + // Original unchanged + expect(task.completed, isFalse); + }); + + test('handles missing id gracefully', () { + final jsObj = JSObject(); + jsObj['title'] = 'No ID'.toJS; + final task = JSTask.fromJS(jsObj); + expect(task.id, equals('')); + }); + + test('handles missing title gracefully', () { + final jsObj = JSObject(); + jsObj['id'] = '1'.toJS; + final task = JSTask.fromJS(jsObj); + expect(task.title, equals('')); + }); + + test('handles missing completed gracefully', () { + final jsObj = JSObject(); + jsObj['id'] = '1'.toJS; + jsObj['title'] = 'Test'.toJS; + final task = JSTask.fromJS(jsObj); + expect(task.completed, isFalse); + }); + }); + + group('JSUser', () { + test('parses name from JSObject', () { + final user = createMockUser(name: 'John Doe'); + expect(user.name, equals('John Doe')); + }); + + test('parses email from JSObject', () { + final user = createMockUser(name: 'John', email: 'john@example.com'); + expect(user.email, equals('john@example.com')); + }); + + test('handles missing name gracefully', () { + final jsObj = JSObject(); + jsObj['email'] = 'test@test.com'.toJS; + final user = JSUser.fromJS(jsObj); + expect(user.name, equals('')); + }); + + test('handles missing email gracefully', () { + final jsObj = JSObject(); + jsObj['name'] = 'Test'.toJS; + final user = JSUser.fromJS(jsObj); + expect(user.email, equals('')); + }); + }); + + group('AuthEffects', () { + test('setToken is callable', () { + var called = false; + JSAny? capturedToken; + + final effects = createMockAuth( + onSetToken: (t) { + called = true; + capturedToken = t; + }, + ); + + effects.setToken('test-token'.toJS); + + expect(called, isTrue); + expect((capturedToken as JSString?)?.toDart, equals('test-token')); + }); + + test('setUser is callable', () { + var called = false; + final effects = createMockAuth(onSetUser: (_) => called = true); + + effects.setUser(createJSObject({'name': 'Test'})); + + expect(called, isTrue); + }); + + test('setView is callable', () { + var capturedView = ''; + final effects = createMockAuth(onSetView: (v) => capturedView = v); + + effects.setView('tasks'); + + expect(capturedView, equals('tasks')); + }); + }); + + group('TaskEffects', () { + test('onToggle receives id and completed status', () { + String? capturedId; + bool? capturedCompleted; + + final effects = ( + onToggle: (String id, bool completed) { + capturedId = id; + capturedCompleted = completed; + }, + onDelete: (String _) {}, + ); + + effects.onToggle('task-1', true); + + expect(capturedId, equals('task-1')); + expect(capturedCompleted, isTrue); + }); + + test('onDelete receives id', () { + String? capturedId; + + final effects = ( + onToggle: (String _, bool __) {}, + onDelete: (String id) => capturedId = id, + ); + + effects.onDelete('task-to-delete'); + + expect(capturedId, equals('task-to-delete')); + }); + }); +} diff --git a/examples/shared/lib/http/http_client.dart b/examples/shared/lib/http/http_client.dart index 5cd1fa9..3455f94 100644 --- a/examples/shared/lib/http/http_client.dart +++ b/examples/shared/lib/http/http_client.dart @@ -5,6 +5,14 @@ import 'package:nadz/nadz.dart'; typedef HttpMethod = String; +typedef Fetch = + Future> Function( + String url, { + HttpMethod method, + String? token, + Map? body, + }); + Future> fetchJson( String url, { HttpMethod method = 'GET', @@ -20,29 +28,23 @@ Future> fetchJson( Future> fetchTasks({ required String token, required String apiUrl, -}) async => - (await fetchJson('$apiUrl/tasks', token: token)).map(_readTaskData); +}) async => (await fetchJson('$apiUrl/tasks', token: token)).map(_readTaskData); -List getObjectKeys(JSObject obj) => _jsObjectKeys(obj) - .toDart - .whereType() - .map((key) => key.toDart) - .toList(); +List getObjectKeys(JSObject obj) => _jsObjectKeys( + obj, +).toDart.whereType().map((key) => key.toDart).toList(); JSObject mapToJsObject(Map map) { final obj = JSObject(); for (final entry in map.entries) { final value = entry.value; - obj.setProperty( - entry.key.toJS, - switch (value) { - final String s => s.toJS, - final bool b => b.toJS, - final num n => n.toJS, - final JSAny jsValue => jsValue, - _ => value.toString().toJS, - }, - ); + obj.setProperty(entry.key.toJS, switch (value) { + final String s => s.toJS, + final bool b => b.toJS, + final num n => n.toJS, + final JSAny jsValue => jsValue, + _ => value.toString().toJS, + }); } return obj; } @@ -68,12 +70,12 @@ external JSString _jsonStringify(JSAny obj); external JSArray _jsObjectKeys(JSObject obj); Map _buildHeaders(String? token) => switch (token) { - null => {'Content-Type': 'application/json'}, - final String t => { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer $t', - }, - }; + null => {'Content-Type': 'application/json'}, + final String t => { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $t', + }, +}; JSObject _createOptions( HttpMethod method, @@ -85,10 +87,7 @@ JSObject _createOptions( ..setProperty('headers'.toJS, mapToJsObject(headers)); switch (body) { case final Map payload: - options.setProperty( - 'body'.toJS, - _jsonStringify(mapToJsObject(payload)), - ); + options.setProperty('body'.toJS, _jsonStringify(mapToJsObject(payload))); } return options; } @@ -112,9 +111,7 @@ Future> _parseResponse(JSObject response) { case JSPromise jsPromise: return _resolveJson(jsPromise); default: - return Future.value( - const Error('json() did not return a promise'), - ); + return Future.value(const Error('json() did not return a promise')); } } @@ -135,16 +132,16 @@ Result _guardSuccess(JSObject json) { } JSArray _readTaskData(JSObject json) => switch (json['data']) { - final JSArray tasks => tasks, - _ => [].toJS, - }; + final JSArray tasks => tasks, + _ => [].toJS, +}; String _readError(JSAny? error) => switch (error) { - final JSString s => s.toDart, - final JSObject obj => - (obj['message'] as JSString?)?.toDart ?? 'Request failed', - _ => 'Request failed', - }; + final JSString s => s.toDart, + final JSObject obj => + (obj['message'] as JSString?)?.toDart ?? 'Request failed', + _ => 'Request failed', +}; String? _extractFieldErrors(JSObject fields) { final keys = getObjectKeys(fields); @@ -152,10 +149,11 @@ String? _extractFieldErrors(JSObject fields) { for (final key in keys) { final value = fields[key]; final messages = switch (value) { - final JSArray arr => arr.toDart - .map((entry) => (entry as JSString?)?.toDart ?? '') - .where((message) => message.isNotEmpty) - .toList(), + final JSArray arr => + arr.toDart + .map((entry) => (entry as JSString?)?.toDart ?? '') + .where((message) => message.isNotEmpty) + .toList(), _ => [], }; errors.addAll(messages.map((msg) => '$key: $msg')); diff --git a/examples/shared/lib/js_types/js_task.dart b/examples/shared/lib/js_types/js_task.dart new file mode 100644 index 0000000..c5ad764 --- /dev/null +++ b/examples/shared/lib/js_types/js_task.dart @@ -0,0 +1,88 @@ +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +/// Type-safe wrapper for JS task objects +extension type JSTask._(JSObject _) implements JSObject { + /// Wrap a JSObject as a JSTask + factory JSTask.fromJS(JSObject js) = JSTask._; + + /// Get the task ID safely + String get id => switch (_['id']) { + final JSString s => s.toDart, + _ => '', + }; + + /// Get the task title safely + String get title => switch (_['title']) { + final JSString s => s.toDart, + _ => '', + }; + + /// Get the task description safely + String? get description => switch (_['description']) { + final JSString s => s.toDart, + _ => null, + }; + + /// Get the completed status safely + bool get completed => switch (_['completed']) { + final JSBoolean b => b.toDart, + _ => false, + }; + + /// Create a copy with updated completed status + JSTask withCompleted(bool value) { + final newTask = JSObject(); + for (final key in _getObjectKeys(_)) { + newTask.setProperty(key.toJS, _[key]); + } + newTask.setProperty('completed'.toJS, value.toJS); + return JSTask._(newTask); + } +} + +/// Get object keys for iteration +List _getObjectKeys(JSObject obj) { + final keys = _objectKeys(obj); + final result = []; + for (var i = 0; i < keys.length; i++) { + final key = keys[i]; + if (key case final JSString s) { + result.add(s.toDart); + } + } + return result; +} + +@JS('Object.keys') +external JSArray _objectKeys(JSObject obj); + +/// Add task only if it doesn't already exist (by ID) +/// Prevents duplicates when both HTTP and WebSocket add the same task +List addTaskIfNotExists(List tasks, JSTask newTask) { + final exists = tasks.any((t) => t.id == newTask.id); + return exists ? tasks : [...tasks, newTask]; +} + +/// Check if a task with the given ID exists in the list +bool taskExists(List tasks, String? id) { + if (id == null) return false; + return tasks.any((t) => t.id == id); +} + +/// Update a task in the list by ID +List updateTaskById(List tasks, JSTask updated) => + tasks.map((t) => t.id == updated.id ? updated : t).toList(); + +/// Remove a task from the list by ID +List removeTaskById(List tasks, String id) => + tasks.where((t) => t.id != id).toList(); + +/// Handle incoming WebSocket task events +List handleTaskEvent(String? type, JSTask task, List current) => + switch (type) { + 'task_created' => addTaskIfNotExists(current, task), + 'task_updated' => updateTaskById(current, task), + 'task_deleted' => removeTaskById(current, task.id), + _ => current, + }; diff --git a/examples/shared/lib/js_types/js_types.dart b/examples/shared/lib/js_types/js_types.dart new file mode 100644 index 0000000..7b03eeb --- /dev/null +++ b/examples/shared/lib/js_types/js_types.dart @@ -0,0 +1,5 @@ +/// Shared JS interop types for frontend and mobile apps +library; + +export 'js_task.dart'; +export 'js_user.dart'; diff --git a/examples/shared/lib/js_types/js_user.dart b/examples/shared/lib/js_types/js_user.dart new file mode 100644 index 0000000..23f8e65 --- /dev/null +++ b/examples/shared/lib/js_types/js_user.dart @@ -0,0 +1,26 @@ +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +/// Type-safe wrapper for JS user objects +extension type JSUser._(JSObject _) implements JSObject { + /// Wrap a JSObject as a JSUser + factory JSUser.fromJS(JSObject js) = JSUser._; + + /// Get the user name safely + String get name => switch (_['name']) { + final JSString s => s.toDart, + _ => '', + }; + + /// Get the user email safely + String get email => switch (_['email']) { + final JSString s => s.toDart, + _ => '', + }; +} + +/// Get display name with fallback to "User" +String getUserDisplayName(JSUser? user) { + final name = user?.name ?? ''; + return name.isEmpty ? 'User' : name; +} diff --git a/examples/shared/lib/models/task.dart b/examples/shared/lib/models/task.dart index 62c8180..5629058 100644 --- a/examples/shared/lib/models/task.dart +++ b/examples/shared/lib/models/task.dart @@ -5,10 +5,10 @@ enum TaskPriority { high; static TaskPriority fromString(String? s) => switch (s) { - 'low' => TaskPriority.low, - 'high' => TaskPriority.high, - _ => TaskPriority.medium, - }; + 'low' => TaskPriority.low, + 'high' => TaskPriority.high, + _ => TaskPriority.medium, + }; } /// Task entity - immutable record @@ -30,30 +30,27 @@ extension TaskExtension on Task { bool? completed, TaskPriority? priority, DateTime? updatedAt, - }) => - ( - id: id, - userId: userId, - title: title ?? this.title, - description: description ?? this.description, - completed: completed ?? this.completed, - priority: priority ?? this.priority, - createdAt: createdAt, - updatedAt: updatedAt ?? this.updatedAt, - ); + }) => ( + id: id, + userId: userId, + title: title ?? this.title, + description: description ?? this.description, + completed: completed ?? this.completed, + priority: priority ?? this.priority, + createdAt: createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); Map toJson() => { - 'id': id, - 'userId': userId, - 'title': title, - ...description != null - ? {'description': description} - : {}, - 'completed': completed, - 'priority': priority.name, - 'createdAt': createdAt.toIso8601String(), - 'updatedAt': updatedAt.toIso8601String(), - }; + 'id': id, + 'userId': userId, + 'title': title, + ...description != null ? {'description': description} : {}, + 'completed': completed, + 'priority': priority.name, + 'createdAt': createdAt.toIso8601String(), + 'updatedAt': updatedAt.toIso8601String(), + }; } /// Data for creating a task diff --git a/examples/shared/lib/models/user.dart b/examples/shared/lib/models/user.dart index 397710c..b2606ca 100644 --- a/examples/shared/lib/models/user.dart +++ b/examples/shared/lib/models/user.dart @@ -30,27 +30,26 @@ extension UserExtension on User { String? name, UserRole? role, DateTime? lastLoginAt, - }) => - ( - id: id, - email: email ?? this.email, - passwordHash: passwordHash ?? this.passwordHash, - name: name ?? this.name, - role: role ?? this.role, - createdAt: createdAt, - lastLoginAt: lastLoginAt ?? this.lastLoginAt, - ); + }) => ( + id: id, + email: email ?? this.email, + passwordHash: passwordHash ?? this.passwordHash, + name: name ?? this.name, + role: role ?? this.role, + createdAt: createdAt, + lastLoginAt: lastLoginAt ?? this.lastLoginAt, + ); Map toJson() => { - 'id': id, - 'email': email, - 'name': name, - 'role': role.name, - 'createdAt': createdAt.toIso8601String(), - ...lastLoginAt != null - ? {'lastLoginAt': lastLoginAt?.toIso8601String()} - : {}, - }; + 'id': id, + 'email': email, + 'name': name, + 'role': role.name, + 'createdAt': createdAt.toIso8601String(), + ...lastLoginAt != null + ? {'lastLoginAt': lastLoginAt?.toIso8601String()} + : {}, + }; } /// Data for user registration @@ -60,8 +59,4 @@ typedef CreateUserData = ({String email, String password, String name}); typedef LoginData = ({String email, String password}); /// Decoded token payload -typedef TokenPayload = ({ - String userId, - DateTime issuedAt, - DateTime expiresAt, -}); +typedef TokenPayload = ({String userId, DateTime issuedAt, DateTime expiresAt}); diff --git a/examples/shared/lib/shared.dart b/examples/shared/lib/shared.dart index 571892d..4e03071 100644 --- a/examples/shared/lib/shared.dart +++ b/examples/shared/lib/shared.dart @@ -2,6 +2,7 @@ library; export 'http/http_client.dart'; +export 'js_types/js_types.dart'; export 'models/task.dart'; export 'models/user.dart'; export 'theme/theme.dart'; diff --git a/examples/shared/lib/theme/styles.dart b/examples/shared/lib/theme/styles.dart index f7396d1..e5091a6 100644 --- a/examples/shared/lib/theme/styles.dart +++ b/examples/shared/lib/theme/styles.dart @@ -43,9 +43,7 @@ abstract final class AppStyles { }; // Form group spacing - static const Map formGroup = { - 'marginBottom': AppSpacing.xl, - }; + static const Map formGroup = {'marginBottom': AppSpacing.xl}; // Primary button static const Map btnPrimary = { @@ -190,9 +188,7 @@ abstract final class AppStyles { }; // Delete button - static const Map deleteBtn = { - 'padding': AppSpacing.sm, - }; + static const Map deleteBtn = {'padding': AppSpacing.sm}; static const Map deleteBtnText = { 'color': AppColors.danger, @@ -415,9 +411,7 @@ abstract final class AppStyles { }; // Spacer (empty element) - static const Map spacer = { - 'display': 'block', - }; + static const Map spacer = {'display': 'block'}; // Large input (for main task input) static const Map inputLg = { diff --git a/packages/dart_node_core/analysis_options.yaml b/packages/dart_node_core/analysis_options.yaml index a4d91f5..75cdfa0 100644 --- a/packages/dart_node_core/analysis_options.yaml +++ b/packages/dart_node_core/analysis_options.yaml @@ -1,2 +1,6 @@ - include: package:austerity/analysis_options.yaml + +analyzer: + errors: + require_trailing_commas: ignore + public_member_api_docs: error diff --git a/packages/dart_node_core/lib/dart_node_core.dart b/packages/dart_node_core/lib/dart_node_core.dart index 6aee2d1..d0fb773 100644 --- a/packages/dart_node_core/lib/dart_node_core.dart +++ b/packages/dart_node_core/lib/dart_node_core.dart @@ -1,5 +1,6 @@ /// Core JS interop utilities for Dart JS Framework library; +export 'src/extensions.dart'; export 'src/interop.dart'; export 'src/node.dart'; diff --git a/packages/dart_node_core/lib/src/extensions.dart b/packages/dart_node_core/lib/src/extensions.dart new file mode 100644 index 0000000..ba4ecc9 --- /dev/null +++ b/packages/dart_node_core/lib/src/extensions.dart @@ -0,0 +1,15 @@ +/// Extension methods FP style transformations +extension NullableExtensions on T? { + /// Pattern match on nullable value with cases for non-null and null. + R match({required R Function(T) some, required R Function() none}) => + switch (this) { + final T value => some(value), + null => none(), + }; +} + +/// Extension methods FP style transformations +extension ObjectExtensions on T { + /// Apply function [op] to this value if non-null and return the result. + R let(R Function(T) op) => op(this); +} diff --git a/packages/dart_node_core/pubspec.yaml b/packages/dart_node_core/pubspec.yaml index 9c6f281..77353f6 100644 --- a/packages/dart_node_core/pubspec.yaml +++ b/packages/dart_node_core/pubspec.yaml @@ -2,6 +2,7 @@ name: dart_node_core description: Core JS interop utilities for dart_node packages version: 0.2.0-beta repository: https://github.com/MelbourneDeveloper/dart_node +publish_to: none environment: sdk: ^3.10.0 diff --git a/packages/dart_node_express/lib/src/errors.dart b/packages/dart_node_express/lib/src/errors.dart index 1bafeb8..d41191e 100644 --- a/packages/dart_node_express/lib/src/errors.dart +++ b/packages/dart_node_express/lib/src/errors.dart @@ -19,9 +19,9 @@ sealed class AppError implements Exception { /// Convert to JSON response format Map toJson() => { - 'success': false, - 'error': {'message': message, 'statusCode': statusCode}, - }; + 'success': false, + 'error': {'message': message, 'statusCode': statusCode}, + }; } /// 400 Bad Request - validation or malformed request @@ -34,16 +34,16 @@ class ValidationError extends AppError { @override Map toJson() => { - ...super.toJson(), - if (fieldErrors.isNotEmpty) 'fieldErrors': fieldErrors, - }; + ...super.toJson(), + if (fieldErrors.isNotEmpty) 'fieldErrors': fieldErrors, + }; } /// 401 Unauthorized - missing or invalid authentication class UnauthorizedError extends AppError { /// Creates an unauthorized error. const UnauthorizedError([String message = 'Unauthorized']) - : super(message, 401); + : super(message, 401); } /// 403 Forbidden - authenticated but not allowed @@ -56,21 +56,21 @@ class ForbiddenError extends AppError { class NotFoundError extends AppError { /// Creates a not found error. const NotFoundError([String resource = 'Resource']) - : super('$resource not found', 404); + : super('$resource not found', 404); } /// 409 Conflict - resource already exists or state conflict class ConflictError extends AppError { /// Creates a conflict error. const ConflictError([String message = 'Resource conflict']) - : super(message, 409); + : super(message, 409); } /// 500 Internal Server Error - unexpected server error class InternalError extends AppError { /// Creates an internal error. const InternalError([String message = 'Internal server error']) - : super(message, 500); + : super(message, 500); } /// Express error handler middleware. @@ -90,15 +90,12 @@ JSFunction errorHandler() => final (int status, Map body) = switch (dartError) { AppError e => (e.statusCode, e.toJson()), _ => ( - 500, - { - 'success': false, - 'error': { - 'message': 'Internal server error', - 'statusCode': 500, - }, - }, - ), + 500, + { + 'success': false, + 'error': {'message': 'Internal server error', 'statusCode': 500}, + }, + ), }; res diff --git a/packages/dart_node_express/lib/src/middleware.dart b/packages/dart_node_express/lib/src/middleware.dart index 91590a2..0661f2e 100644 --- a/packages/dart_node_express/lib/src/middleware.dart +++ b/packages/dart_node_express/lib/src/middleware.dart @@ -8,11 +8,8 @@ import 'package:dart_node_express/src/response.dart'; typedef NextFunction = void Function(); /// Type for synchronous middleware -typedef MiddlewareHandler = void Function( - Request req, - Response res, - NextFunction next, -); +typedef MiddlewareHandler = + void Function(Request req, Response res, NextFunction next); /// Converts a Dart middleware to JS function. JSFunction middleware(MiddlewareHandler handler) => diff --git a/packages/dart_node_express/lib/src/validation.dart b/packages/dart_node_express/lib/src/validation.dart index a31b716..0591c82 100644 --- a/packages/dart_node_express/lib/src/validation.dart +++ b/packages/dart_node_express/lib/src/validation.dart @@ -80,12 +80,12 @@ class StringValidator extends Validator { ValidationResult validate(dynamic value) { if (value == null) { return Invalid({ - _fieldName: ['is required'] + _fieldName: ['is required'], }); } if (value is! String) { return Invalid({ - _fieldName: ['must be a string'] + _fieldName: ['must be a string'], }); } @@ -108,35 +108,29 @@ class StringValidator extends Validator { /// Minimum length StringValidator minLength(int min) => _addCheck( - (v) => v.length < min ? 'must be at least $min characters' : '', - ); + (v) => v.length < min ? 'must be at least $min characters' : '', + ); /// Maximum length - StringValidator maxLength(int max) => _addCheck( - (v) => v.length > max ? 'must be at most $max characters' : '', - ); + StringValidator maxLength(int max) => + _addCheck((v) => v.length > max ? 'must be at most $max characters' : ''); /// Must not be empty - StringValidator notEmpty() => _addCheck( - (v) => v.isEmpty ? 'must not be empty' : '', - ); + StringValidator notEmpty() => + _addCheck((v) => v.isEmpty ? 'must not be empty' : ''); /// Must match pattern StringValidator matches(RegExp pattern, [String? message]) => _addCheck( - (v) => !pattern.hasMatch(v) ? (message ?? 'invalid format') : '', - ); + (v) => !pattern.hasMatch(v) ? (message ?? 'invalid format') : '', + ); /// Must be a valid email - StringValidator email() => matches( - RegExp(r'^[\w\.-]+@[\w\.-]+\.\w+$'), - 'must be a valid email', - ); + StringValidator email() => + matches(RegExp(r'^[\w\.-]+@[\w\.-]+\.\w+$'), 'must be a valid email'); /// Must be alphanumeric - StringValidator alphanumeric() => matches( - RegExp(r'^[a-zA-Z0-9]+$'), - 'must be alphanumeric', - ); + StringValidator alphanumeric() => + matches(RegExp(r'^[a-zA-Z0-9]+$'), 'must be alphanumeric'); } // ============================================================================ @@ -156,7 +150,7 @@ class IntValidator extends Validator { ValidationResult validate(dynamic value) { if (value == null) { return Invalid({ - _fieldName: ['is required'] + _fieldName: ['is required'], }); } @@ -167,7 +161,7 @@ class IntValidator extends Validator { final parsed = int.tryParse(value); if (parsed == null) { return Invalid({ - _fieldName: ['must be a number'] + _fieldName: ['must be a number'], }); } intValue = parsed; @@ -175,7 +169,7 @@ class IntValidator extends Validator { intValue = value.toInt(); } else { return Invalid({ - _fieldName: ['must be a number'] + _fieldName: ['must be a number'], }); } @@ -197,22 +191,18 @@ class IntValidator extends Validator { } /// Minimum value - IntValidator min(int min) => _addCheck( - (v) => v < min ? 'must be at least $min' : '', - ); + IntValidator min(int min) => + _addCheck((v) => v < min ? 'must be at least $min' : ''); /// Maximum value - IntValidator max(int max) => _addCheck( - (v) => v > max ? 'must be at most $max' : '', - ); + IntValidator max(int max) => + _addCheck((v) => v > max ? 'must be at most $max' : ''); /// Must be in range IntValidator range(int min, int max) => this.min(min).max(max); /// Must be positive - IntValidator positive() => _addCheck( - (v) => v <= 0 ? 'must be positive' : '', - ); + IntValidator positive() => _addCheck((v) => v <= 0 ? 'must be positive' : ''); } // ============================================================================ @@ -231,7 +221,7 @@ class BoolValidator extends Validator { ValidationResult validate(dynamic value) { if (value == null) { return Invalid({ - _fieldName: ['is required'] + _fieldName: ['is required'], }); } if (value is bool) { @@ -242,7 +232,7 @@ class BoolValidator extends Validator { if (value.toLowerCase() == 'false') return Valid(false); } return Invalid({ - _fieldName: ['must be a boolean'] + _fieldName: ['must be a boolean'], }); } } @@ -279,8 +269,7 @@ class OptionalValidator extends Validator { Schema schema( Map fields, T Function(Map) constructor, -) => - Schema(fields, constructor); +) => Schema(fields, constructor); class Schema extends Validator { final Map fields; @@ -292,7 +281,7 @@ class Schema extends Validator { ValidationResult validate(dynamic value) { if (value == null) { return const Invalid({ - 'body': ['is required'] + 'body': ['is required'], }); } @@ -304,7 +293,7 @@ class Schema extends Validator { map = (value.dartify() as Map).cast(); } else { return const Invalid({ - 'body': ['must be an object'] + 'body': ['must be an object'], }); } @@ -356,10 +345,7 @@ JSFunction validateBody(Schema schema) { next(); case Invalid(:final errors): res.status(400); - res.jsonMap({ - 'error': 'Validation failed', - 'fields': errors, - }); + res.jsonMap({'error': 'Validation failed', 'fields': errors}); } }).toJS; } diff --git a/packages/dart_node_express/pubspec.lock b/packages/dart_node_express/pubspec.lock index 8852cc9..7a3b360 100644 --- a/packages/dart_node_express/pubspec.lock +++ b/packages/dart_node_express/pubspec.lock @@ -12,10 +12,9 @@ packages: dart_node_core: dependency: "direct main" description: - name: dart_node_core - sha256: "4926d74031883ed7948018ba47604270015571e4e9035db1f4bdf59e18dbd58b" - url: "https://pub.dev" - source: hosted + path: "../dart_node_core" + relative: true + source: path version: "0.2.0-beta" nadz: dependency: transitive diff --git a/packages/dart_node_express/pubspec.yaml b/packages/dart_node_express/pubspec.yaml index 1423826..c658095 100644 --- a/packages/dart_node_express/pubspec.yaml +++ b/packages/dart_node_express/pubspec.yaml @@ -2,10 +2,12 @@ name: dart_node_express description: Express.js bindings for Dart version: 0.2.0-beta repository: https://github.com/MelbourneDeveloper/dart_node +publish_to: none environment: sdk: ^3.10.0 dependencies: austerity: ^1.3.0 - dart_node_core: ^0.2.0-beta + dart_node_core: + path: ../dart_node_core diff --git a/packages/dart_node_react/analysis_options.yaml b/packages/dart_node_react/analysis_options.yaml index bc3aeb4..75cdfa0 100644 --- a/packages/dart_node_react/analysis_options.yaml +++ b/packages/dart_node_react/analysis_options.yaml @@ -1,6 +1,6 @@ - include: package:austerity/analysis_options.yaml analyzer: errors: - require_trailing_commas: ignore \ No newline at end of file + require_trailing_commas: ignore + public_member_api_docs: error diff --git a/packages/dart_node_react/dart_test.yaml b/packages/dart_node_react/dart_test.yaml new file mode 100644 index 0000000..1d9e304 --- /dev/null +++ b/packages/dart_node_react/dart_test.yaml @@ -0,0 +1,8 @@ +platforms: [chrome] + +override_platforms: + chrome: + settings: + arguments: --disable-gpu --no-sandbox + +custom_html_template_path: test/test_template.html diff --git a/packages/dart_node_react/lib/dart_node_react.dart b/packages/dart_node_react/lib/dart_node_react.dart index 0d671fe..9bd0e36 100644 --- a/packages/dart_node_react/lib/dart_node_react.dart +++ b/packages/dart_node_react/lib/dart_node_react.dart @@ -1,7 +1,18 @@ /// React bindings for Dart library; +export 'src/children.dart'; +export 'src/context.dart'; export 'src/elements.dart'; +export 'src/function_component.dart'; export 'src/hooks.dart'; +export 'src/html_elements.dart'; export 'src/react.dart'; export 'src/react_dom.dart'; +export 'src/reducer_hook.dart'; +export 'src/ref.dart'; +export 'src/special_components.dart'; +export 'src/svg_elements.dart'; +export 'src/synthetic_event.dart'; +export 'src/test_utils.dart'; +export 'src/testing_library.dart' hide render; diff --git a/packages/dart_node_react/lib/src/children.dart b/packages/dart_node_react/lib/src/children.dart new file mode 100644 index 0000000..f303dc1 --- /dev/null +++ b/packages/dart_node_react/lib/src/children.dart @@ -0,0 +1,146 @@ +/// React.Children utilities for manipulating children props. +/// +/// Provides type-safe wrappers around React.Children API methods. +library; + +import 'dart:js_interop'; + +import 'package:dart_node_react/src/react.dart'; + +// ============================================================================= +// React.Children JS Bindings +// ============================================================================= + +@JS('React.Children.map') +external JSArray? _childrenMap(JSAny? children, JSFunction fn); + +@JS('React.Children.forEach') +external void _childrenForEach(JSAny? children, JSFunction fn); + +@JS('React.Children.count') +external int _childrenCount(JSAny? children); + +@JS('React.Children.only') +external JSObject _childrenOnly(JSAny? children); + +@JS('React.Children.toArray') +external JSArray _childrenToArray(JSAny? children); + +// ============================================================================= +// Children Utilities +// ============================================================================= + +/// Utilities for dealing with the children prop. +/// +/// React.Children provides utilities for dealing with the opaque data structure +/// of props.children. +/// +/// Example: +/// ```dart +/// final MyComponent = registerFunctionComponent((props) { +/// final children = props['children']; +/// final count = Children.count(children); +/// return div(children: [ +/// pEl('This component has $count children'), +/// ...Children.toArray(children), +/// ]); +/// }); +/// ``` +/// +/// See: https://react.dev/reference/react/Children + +// This class mirrors React's Children API which is a namespace with static +// methods, so the lint is intentionally ignored. +// ignore_for_file: avoid_classes_with_only_static_members +abstract final class Children { + /// Calls a function for each child in children. + /// + /// If children is null or undefined, returns null. Otherwise, returns a new + /// array with the results of calling fn on each child. + /// + /// Example: + /// ```dart + /// final items = Children.map(props['children'], (child, index) { + /// return cloneElement(child, {'key': 'item-$index'}); + /// }); + /// ``` + /// + /// See: https://react.dev/reference/react/Children#children-map + static List? map( + JSAny? children, + ReactElement Function(ReactElement child, int index) fn, + ) { + ReactElement jsMapper(JSObject child, JSNumber index) => + fn(ReactElement.fromJS(child), index.toDartInt); + + final result = _childrenMap(children, jsMapper.toJS); + return result?.toDart + .map((e) => ReactElement.fromJS(e! as JSObject)) + .toList(); + } + + /// Iterates over children, calling fn for each child. + /// + /// Unlike map, forEach does not return anything. + /// + /// Example: + /// ```dart + /// Children.forEach(props['children'], (child, index) { + /// print('Child $index: ${child.type}'); + /// }); + /// ``` + /// + /// See: https://react.dev/reference/react/Children#children-foreach + static void forEach( + JSAny? children, + void Function(ReactElement child, int index) fn, + ) { + void jsIterator(JSObject child, JSNumber index) => + fn(ReactElement.fromJS(child), index.toDartInt); + + _childrenForEach(children, jsIterator.toJS); + } + + /// Returns the total number of children. + /// + /// Empty nodes (null, undefined, and Booleans), strings, numbers, and React + /// elements count as individual nodes. Arrays don't count as individual + /// nodes, but their children do. + /// + /// Example: + /// ```dart + /// final childCount = Children.count(props['children']); + /// ``` + /// + /// See: https://react.dev/reference/react/Children#children-count + static int count(JSAny? children) => _childrenCount(children); + + /// Verifies that children has only one child and returns it. + /// + /// Throws if children has zero or more than one child. + /// + /// Example: + /// ```dart + /// final onlyChild = Children.only(props['children']); + /// ``` + /// + /// See: https://react.dev/reference/react/Children#children-only + static ReactElement only(JSAny? children) => + ReactElement.fromJS(_childrenOnly(children)); + + /// Returns children as a flat array. + /// + /// Useful when you need to manipulate the children with standard array + /// methods like filter, sort, or reverse. + /// + /// Example: + /// ```dart + /// final childArray = Children.toArray(props['children']); + /// final reversed = childArray.reversed.toList(); + /// ``` + /// + /// See: https://react.dev/reference/react/Children#children-toarray + static List toArray(JSAny? children) => _childrenToArray( + children, + ).toDart.map((e) => ReactElement.fromJS(e! as JSObject)).toList(); +} diff --git a/packages/dart_node_react/lib/src/context.dart b/packages/dart_node_react/lib/src/context.dart new file mode 100644 index 0000000..5517619 --- /dev/null +++ b/packages/dart_node_react/lib/src/context.dart @@ -0,0 +1,140 @@ +import 'dart:js_interop'; + +// ============================================================================= +// React Context JS Bindings +// ============================================================================= + +@JS('React.createContext') +external JSObject _reactCreateContext([JSAny? defaultValue]); + +@JS('React.useContext') +external JSAny? _reactUseContext(JSObject context); + +/// A JavaScript context object returned by React.createContext(). +extension type JsContext._(JSObject _) implements JSObject { + /// Creates a JsContext from a raw JSObject. + factory JsContext.fromJs(JSObject jsObject) = JsContext._; + + /// The Provider component for this context. + @JS('Provider') + external JSAny get provider; + + /// The Consumer component for this context (deprecated, use useContext). + @JS('Consumer') + external JSAny get consumer; +} + +/// A Context object created by [createContext]. +/// +/// Every Context object comes with a Provider React component that allows +/// consuming components to subscribe to context changes. +/// +/// See: https://reactjs.org/docs/context.html +final class Context { + Context._(this._jsContext, this._defaultValue); + + final JsContext _jsContext; + final T _defaultValue; + + /// The JavaScript context object. + JsContext get jsContext => _jsContext; + + /// The default value provided to createContext. + T get defaultValue => _defaultValue; + + /// The Provider component type for this context. + /// + /// Use this with createElement to wrap components that need access to + /// this context. + /// + /// Example: + /// ```dart + /// final ThemeContext = createContext('light'); + /// + /// // Create a provider + /// createElement( + /// ThemeContext.providerType, + /// createProps({'value': 'dark'}), + /// childElement, + /// ); + /// ``` + JSAny get providerType => _jsContext.provider; + + /// The Consumer component type for this context. + /// + /// Note: The Consumer component is considered legacy. Prefer [useContext] + /// hook instead. + JSAny get consumerType => _jsContext.consumer; +} + +/// Creates a Context object with the specified [defaultValue]. +/// +/// When React renders a component that subscribes to this Context object +/// it will read the current context value from the closest matching Provider +/// above it in the tree. +/// +/// The [defaultValue] argument is only used when a component does not have a +/// matching Provider above it in the tree. +/// +/// Example: +/// ```dart +/// // Create a context with a default value +/// final ThemeContext = createContext('light'); +/// +/// // In a parent component, provide a value +/// final provider = createElement( +/// ThemeContext.providerType, +/// createProps({'value': 'dark'}), +/// MyComponent(), +/// ); +/// +/// // In a child function component, consume the value +/// final MyComponent = registerFunctionComponent((props) { +/// final theme = useContext(ThemeContext); +/// return div(children: [ +/// pEl('Current theme: $theme'), +/// ]); +/// }); +/// ``` +/// +/// See: https://reactjs.org/docs/context.html#reactcreatecontext +Context createContext(T defaultValue) { + final jsDefault = (defaultValue == null) + ? null + : (defaultValue as Object).jsify(); + final jsContext = JsContext.fromJs(_reactCreateContext(jsDefault)); + return Context._(jsContext, defaultValue); +} + +/// Accepts a [Context] object and returns the current context value for that +/// context. +/// +/// The current context value is determined by the value prop of the nearest +/// `` above the calling component in the tree. +/// +/// When the nearest `` above the component updates, this +/// Hook will trigger a rerender with the latest context value passed to that +/// provider. +/// +/// Note: there are two rules for using Hooks: +/// - Only call Hooks at the top level. +/// - Only call Hooks from inside a function component. +/// +/// Example: +/// ```dart +/// final ThemeContext = createContext('light'); +/// +/// final ThemedButton = registerFunctionComponent((props) { +/// final theme = useContext(ThemeContext); +/// return button( +/// text: 'Click me', +/// style: {'background': theme == 'dark' ? '#333' : '#fff'}, +/// ); +/// }); +/// ``` +/// +/// See: https://reactjs.org/docs/hooks-reference.html#usecontext +T useContext(Context context) { + final jsValue = _reactUseContext(context.jsContext); + return (jsValue == null) ? context.defaultValue : jsValue.dartify() as T; +} diff --git a/packages/dart_node_react/lib/src/elements.dart b/packages/dart_node_react/lib/src/elements.dart index bc35bae..3feadf5 100644 --- a/packages/dart_node_react/lib/src/elements.dart +++ b/packages/dart_node_react/lib/src/elements.dart @@ -2,6 +2,7 @@ import 'dart:js_interop'; import 'dart:js_interop_unsafe'; import 'package:dart_node_react/src/react.dart'; +import 'package:dart_node_react/src/synthetic_event.dart'; // ============================================================================= // Typed HTML Elements - Each element gets its own type for compile-time safety @@ -107,23 +108,23 @@ JSObject convertStyle(Map style) { final jsValue = (value is num && _needsPxSuffix(entry.key)) ? '${value}px'.toJS : (value is String) - ? value.toJS - : (value is num) - ? value.toJS - : (value as Object).jsify(); + ? value.toJS + : (value is num) + ? value.toJS + : (value as Object).jsify(); obj.setProperty(entry.key.toJS, jsValue); } return obj; } bool _needsPxSuffix(String key) => !const { - 'flex', - 'fontWeight', - 'opacity', - 'zIndex', - 'lineHeight', - 'order', - }.contains(key); + 'flex', + 'fontWeight', + 'opacity', + 'zIndex', + 'lineHeight', + 'order', +}.contains(key); Map _buildProps({ Map? props, @@ -149,8 +150,8 @@ DivElement div({ final jsObj = (children != null && children.isNotEmpty) ? createElementWithChildren('div'.toJS, _propsOrNull(p), children) : (child != null) - ? createElement('div'.toJS, _propsOrNull(p), child) - : createElement('div'.toJS, _propsOrNull(p)); + ? createElement('div'.toJS, _propsOrNull(p), child) + : createElement('div'.toJS, _propsOrNull(p)); return DivElement.fromJS(jsObj); } @@ -220,7 +221,10 @@ InputElement input({ String? type, String? value, String? placeholder, - void Function(JSAny)? onChange, + void Function(SyntheticEvent)? onChange, + void Function(SyntheticEvent)? onInput, + void Function(SyntheticFocusEvent)? onFocus, + void Function(SyntheticFocusEvent)? onBlur, Map? props, Map? style, String? className, @@ -229,7 +233,22 @@ InputElement input({ if (type != null) p['type'] = type; if (value != null) p['value'] = value; if (placeholder != null) p['placeholder'] = placeholder; - if (onChange != null) p['onChange'] = onChange; + if (onChange != null) { + void handler(JSObject e) => onChange(SyntheticEvent.fromJs(e)); + p['onChange'] = handler; + } + if (onInput != null) { + void handler(JSObject e) => onInput(SyntheticEvent.fromJs(e)); + p['onInput'] = handler; + } + if (onFocus != null) { + void handler(JSObject e) => onFocus(SyntheticFocusEvent.fromJs(e)); + p['onFocus'] = handler; + } + if (onBlur != null) { + void handler(JSObject e) => onBlur(SyntheticFocusEvent.fromJs(e)); + p['onBlur'] = handler; + } return InputElement.fromJS(createElement('input'.toJS, createProps(p))); } @@ -297,8 +316,8 @@ HeaderElement header({ final jsObj = (children != null && children.isNotEmpty) ? createElementWithChildren('header'.toJS, _propsOrNull(p), children) : (child != null) - ? createElement('header'.toJS, _propsOrNull(p), child) - : createElement('header'.toJS, _propsOrNull(p)); + ? createElement('header'.toJS, _propsOrNull(p), child) + : createElement('header'.toJS, _propsOrNull(p)); return HeaderElement.fromJS(jsObj); } @@ -314,8 +333,8 @@ MainElement mainEl({ final jsObj = (children != null && children.isNotEmpty) ? createElementWithChildren('main'.toJS, _propsOrNull(p), children) : (child != null) - ? createElement('main'.toJS, _propsOrNull(p), child) - : createElement('main'.toJS, _propsOrNull(p)); + ? createElement('main'.toJS, _propsOrNull(p), child) + : createElement('main'.toJS, _propsOrNull(p)); return MainElement.fromJS(jsObj); } @@ -331,7 +350,7 @@ FooterElement footer({ final jsObj = (children != null && children.isNotEmpty) ? createElementWithChildren('footer'.toJS, _propsOrNull(p), children) : (child != null) - ? createElement('footer'.toJS, _propsOrNull(p), child) - : createElement('footer'.toJS, _propsOrNull(p)); + ? createElement('footer'.toJS, _propsOrNull(p), child) + : createElement('footer'.toJS, _propsOrNull(p)); return FooterElement.fromJS(jsObj); } diff --git a/packages/dart_node_react/lib/src/function_component.dart b/packages/dart_node_react/lib/src/function_component.dart new file mode 100644 index 0000000..c1b3cc8 --- /dev/null +++ b/packages/dart_node_react/lib/src/function_component.dart @@ -0,0 +1,79 @@ +/// Function component registration for React. +/// +/// Provides typed wrappers for creating React function components with Dart. +library; + +import 'dart:js_interop'; + +import 'package:dart_node_react/src/react.dart'; + +/// A Dart function component that takes a props Map and returns a ReactElement. +typedef DartFunctionComponent = + ReactElement Function(Map props); + +/// Creates a React function component from a Dart function. +/// +/// This wraps a Dart function that takes a `Map` of props +/// and returns a `ReactElement`. The wrapper handles converting between +/// JS and Dart types automatically. +/// +/// Example: +/// ```dart +/// final Counter = registerFunctionComponent((props) { +/// final count = useState(props['initialCount'] as int? ?? 0); +/// +/// return div(children: [ +/// pEl('Count: ${count.value}'), +/// button( +/// text: 'Increment', +/// onClick: () => count.setWithUpdater((prev) => prev + 1), +/// ), +/// ]); +/// }); +/// +/// // Use in render: +/// createElement(Counter, createProps({'initialCount': 5})); +/// ``` +/// +/// You can optionally provide a [displayName] for better debugging in React +/// DevTools. +JSAny registerFunctionComponent( + DartFunctionComponent dartComponent, { + String? displayName, +}) { + ReactElement jsComponent(JSObject jsProps) { + final dartified = jsProps.dartify(); + final props = (dartified is Map) + ? dartified.cast() + : {}; + return dartComponent(props); + } + + final component = jsComponent.toJS; + return component; +} + +/// Creates a ReactElement from a registered function component. +/// +/// This is a convenience function that combines creating props and the element. +/// +/// Example: +/// ```dart +/// final MyComponent = registerFunctionComponent((props) { +/// return pEl('Hello ${props['name']}!'); +/// }); +/// +/// // Usage: +/// fc(MyComponent, {'name': 'World'}); +/// ``` +ReactElement fc( + JSAny component, [ + Map? props, + List? children, +]) => (children != null && children.isNotEmpty) + ? createElementWithChildren( + component, + props != null ? createProps(props) : null, + children, + ) + : createElement(component, props != null ? createProps(props) : null); diff --git a/packages/dart_node_react/lib/src/hooks.dart b/packages/dart_node_react/lib/src/hooks.dart index 2d977fa..ee1c736 100644 --- a/packages/dart_node_react/lib/src/hooks.dart +++ b/packages/dart_node_react/lib/src/hooks.dart @@ -1,29 +1,373 @@ import 'dart:js_interop'; import 'package:dart_node_react/src/react.dart'; +import 'package:dart_node_react/src/ref.dart'; -/// useState hook - returns (value, setter) -(JSAny?, JSFunction) useState(JSAny? initialValue) { - final result = React.useState(initialValue); - final setter = result[1]; - return switch (setter) { - final JSFunction fn => (result[0], fn), - _ => throw StateError('useState setter is null'), +export 'package:dart_node_react/src/reducer_hook.dart'; +// Re-export StateHook and ReducerHook from their own files +export 'package:dart_node_react/src/state_hook.dart'; + +// ============================================================================= +// JS undefined constant for cleanup function returns +// ============================================================================= + +@JS('undefined') +external JSAny? get _jsUndefined; + +// ============================================================================= +// Additional React hook bindings +// ============================================================================= + +@JS('React.useLayoutEffect') +external void _reactUseLayoutEffect(JSFunction effect, [JSArray? deps]); + +@JS('React.useImperativeHandle') +external void _reactUseImperativeHandle( + JSAny? ref, + JSFunction createHandle, [ + JSArray? deps, +]); + +@JS('React.useDebugValue') +external void _reactUseDebugValue(JSAny? value, [JSFunction? format]); + +// ============================================================================= +// Helper to convert dart values to JS +// ============================================================================= + +JSAny? _toJsAny(Object? value) => (value == null) ? null : value.jsify(); + +// ============================================================================= +// Effect Hooks +// ============================================================================= + +/// Runs [sideEffect] after every completed render of a function component. +/// +/// If [dependencies] are given, [sideEffect] will only run if one of the +/// [dependencies] have changed. [sideEffect] may return a cleanup function +/// that is run before the component unmounts or re-renders. +/// +/// Note: there are two rules for using Hooks: +/// - Only call Hooks at the top level. +/// - Only call Hooks from inside a function component. +/// +/// Example: +/// ```dart +/// final MyComponent = registerFunctionComponent((props) { +/// final count = useState(1); +/// final evenOdd = useState('even'); +/// +/// useEffect(() { +/// evenOdd.set(count.value % 2 == 0 ? 'even' : 'odd'); +/// return () { +/// print('cleanup: count is changing...'); +/// }; +/// }, [count.value]); +/// +/// return div(children: [ +/// pEl('\${count.value} is \${evenOdd.value}'), +/// button(text: '+', onClick: () => count.set(count.value + 1)), +/// ]); +/// }); +/// ``` +/// +/// Learn more: https://reactjs.org/docs/hooks-effect.html +void useEffect(Object? Function() sideEffect, [List? dependencies]) { + JSAny? wrappedSideEffect() { + final result = sideEffect(); + return (result is void Function()) ? result.toJS : _jsUndefined; + } + + final jsDeps = dependencies?.map(_toJsAny).toList().toJS; + React.useEffect(wrappedSideEffect.toJS, jsDeps); +} + +/// Runs [sideEffect] synchronously after a function component renders, but +/// before the screen is updated. +/// +/// Compare to [useEffect] which runs [sideEffect] after the screen updates. +/// Prefer the standard [useEffect] when possible to avoid blocking visual +/// updates. +/// +/// Note: there are two rules for using Hooks: +/// - Only call Hooks at the top level. +/// - Only call Hooks from inside a function component. +/// +/// Example: +/// ```dart +/// final MyComponent = registerFunctionComponent((props) { +/// final width = useState(0); +/// final textareaRef = useRef(null); +/// +/// useLayoutEffect(() { +/// width.set(textareaRef.current?.clientWidth ?? 0); +/// }); +/// +/// return div(children: [ +/// pEl('textarea width: \${width.value}'), +/// textarea({'ref': textareaRef.jsRef}), +/// ]); +/// }); +/// ``` +/// +/// Learn more: https://reactjs.org/docs/hooks-reference.html#uselayouteffect +void useLayoutEffect( + Object? Function() sideEffect, [ + List? dependencies, +]) { + JSAny? wrappedSideEffect() { + final result = sideEffect(); + return (result is void Function()) ? result.toJS : _jsUndefined; + } + + final jsDeps = dependencies?.map(_toJsAny).toList().toJS; + _reactUseLayoutEffect(wrappedSideEffect.toJS, jsDeps); +} + +// ============================================================================= +// Ref Hooks +// ============================================================================= + +/// Returns an empty mutable [Ref] object. +/// +/// To initialize a ref with a value, use [useRefInit] instead. +/// +/// Changes to the [Ref.current] property do not cause the containing +/// function component to re-render. +/// +/// The returned [Ref] object will persist for the full lifetime of the +/// function component. Compare to [createRef] which returns a new [Ref] +/// object on each render. +/// +/// Note: there are two rules for using Hooks: +/// - Only call Hooks at the top level. +/// - Only call Hooks from inside a function component. +/// +/// Example: +/// ```dart +/// final MyComponent = registerFunctionComponent((props) { +/// final inputRef = useRef(null); +/// +/// return div(children: [ +/// input({'ref': inputRef.jsRef}), +/// button(text: 'Focus', onClick: () => inputRef.current?.focus()), +/// ]); +/// }); +/// ``` +/// +/// Learn more: https://reactjs.org/docs/hooks-reference.html#useref +Ref useRef([T? initialValue]) => useRefInit(initialValue); + +/// Returns a mutable [Ref] object with [Ref.current] property initialized to +/// [initialValue]. +/// +/// Changes to the [Ref.current] property do not cause the containing +/// function component to re-render. +/// +/// The returned [Ref] object will persist for the full lifetime of the +/// function component. Compare to [createRef] which returns a new [Ref] +/// object on each render. +/// +/// Note: there are two rules for using Hooks: +/// - Only call Hooks at the top level. +/// - Only call Hooks from inside a function component. +/// +/// Example: +/// ```dart +/// final MyComponent = registerFunctionComponent((props) { +/// final countRef = useRefInit(1); +/// +/// void handleClick([_]) { +/// countRef.current = countRef.current + 1; +/// print('You clicked \${countRef.current} times!'); +/// } +/// +/// return button(text: 'Click me!', onClick: handleClick); +/// }); +/// ``` +/// +/// Learn more: https://reactjs.org/docs/hooks-reference.html#useref +Ref useRefInit(T initialValue) { + final jsInitial = _toJsAny(initialValue); + final jsRef = React.useRef(jsInitial); + return Ref.fromJs(JsRef.fromJs(jsRef)); +} + +/// Customizes the [ref] value that is exposed to parent components when using +/// forwardRef2 by setting `ref.current` to the return value of +/// `createHandle`. +/// +/// In most cases, imperative code using refs should be avoided. +/// +/// Note: there are two rules for using Hooks: +/// - Only call Hooks at the top level. +/// - Only call Hooks from inside a function component. +/// +/// Example: +/// ```dart +/// class FancyInputApi { +/// final void Function() focus; +/// FancyInputApi(this.focus); +/// } +/// +/// final FancyInput = forwardRef2((props, ref) { +/// final inputRef = useRef(null); +/// +/// useImperativeHandle( +/// ref, +/// () => FancyInputApi(() => inputRef.current?.focus()), +/// [], +/// ); +/// +/// return input({ +/// 'ref': inputRef.jsRef, +/// 'value': props['value'], +/// }); +/// }); +/// ``` +/// +/// Learn more: https://reactjs.org/docs/hooks-reference.html#useimperativehandle +void useImperativeHandle( + Object? ref, + T Function() createHandle, [ + List? dependencies, +]) { + JSAny? jsCreateHandle() => _toJsAny(createHandle()); + + // ref will be a JsRef in forwardRef2, or a Ref in forwardRef. + final jsRef = switch (ref) { + final Ref r => r.jsRef, + final JsRef jr => jr, + _ => ref as JSAny?, }; + + final jsDeps = dependencies?.map(_toJsAny).toList().toJS; + _reactUseImperativeHandle(jsRef, jsCreateHandle.toJS, jsDeps); } -/// useEffect hook -void useEffect(JSFunction effect, [JSArray? deps]) { - React.useEffect(effect, deps); +// ============================================================================= +// Memoization Hooks +// ============================================================================= + +/// Returns a memoized version of the return value of [createFunction]. +/// +/// If one of the [dependencies] has changed, [createFunction] is run during +/// rendering of the function component. This optimization helps to avoid +/// expensive calculations on every render. +/// +/// Note: there are two rules for using Hooks: +/// - Only call Hooks at the top level. +/// - Only call Hooks from inside a function component. +/// +/// Example: +/// ```dart +/// final MyComponent = registerFunctionComponent((props) { +/// final count = useState(0); +/// +/// final fib = useMemo( +/// () => fibonacci(count.value), +/// [count.value], +/// ); +/// +/// return div(children: [ +/// pEl('Fibonacci of \${count.value} is \$fib'), +/// button( +/// text: '+', +/// onClick: () => count.setWithUpdater((prev) => prev + 1), +/// ), +/// ]); +/// }); +/// ``` +/// +/// Learn more: https://reactjs.org/docs/hooks-reference.html#usememo +T useMemo(T Function() createFunction, [List? dependencies]) { + JSAny? jsCreateFunction() => _toJsAny(createFunction()); + + final jsDeps = (dependencies ?? []).map(_toJsAny).toList().toJS; + final result = React.useMemo(jsCreateFunction.toJS, jsDeps); + return result.dartify() as T; } -/// useRef hook -JSObject useRef(JSAny? initialValue) => React.useRef(initialValue); +/// Returns a memoized version of [callback] that only changes if one of the +/// [dependencies] has changed. +/// +/// Note: there are two rules for using Hooks: +/// - Only call Hooks at the top level. +/// - Only call Hooks from inside a function component. +/// +/// Example: +/// ```dart +/// final MyComponent = registerFunctionComponent((props) { +/// final count = useState(0); +/// final delta = useState(1); +/// +/// final increment = useCallback((_) { +/// count.setWithUpdater((prev) => prev + delta.value); +/// }, [delta.value]); +/// +/// return div(children: [ +/// pEl('Delta is \${delta.value}'), +/// pEl('Count is \${count.value}'), +/// button(text: 'Increment count', onClick: increment), +/// ]); +/// }); +/// ``` +/// +/// Learn more: https://reactjs.org/docs/hooks-reference.html#usecallback +JSFunction useCallback(Function callback, List dependencies) { + final jsCallback = (callback is void Function()) + ? callback.toJS + : (callback is void Function(JSAny)) + ? callback.toJS + : throw StateError('Unsupported callback type: ${callback.runtimeType}'); -/// useMemo hook -JSAny? useMemo(JSFunction factory, JSArray deps) => - React.useMemo(factory, deps); + final jsDeps = dependencies.map(_toJsAny).toList().toJS; + return React.useCallback(jsCallback, jsDeps); +} -/// useCallback hook -JSFunction useCallback(JSFunction callback, JSArray deps) => - React.useCallback(callback, deps); +// ============================================================================= +// Debug Hooks +// ============================================================================= + +/// Displays [value] as a label for a custom hook in React DevTools. +/// +/// To defer formatting [value] until the hooks are inspected, use the optional +/// [format] function. +/// +/// Note: there are two rules for using Hooks: +/// - Only call Hooks at the top level. +/// - Only call Hooks from inside a function component. +/// +/// Example: +/// ```dart +/// // Custom Hook +/// StateHook useFriendStatus(int friendID) { +/// final isOnline = useState(false); +/// +/// useEffect(() { +/// ChatAPI.subscribeToFriendStatus(friendID, (status) { +/// isOnline.set(status['isOnline']); +/// }); +/// return () { +/// ChatAPI.unsubscribeFromFriendStatus(friendID); +/// }; +/// }); +/// +/// // Use format function to avoid unnecessarily formatting isOnline +/// useDebugValue( +/// isOnline.value, +/// (isOnline) => isOnline ? 'Online' : 'Not Online', +/// ); +/// +/// return isOnline; +/// } +/// ``` +/// +/// Learn more: https://reactjs.org/docs/hooks-reference.html#usedebugvalue +void useDebugValue(T value, [String Function(T)? format]) { + final jsValue = _toJsAny(value); + JSString jsFormatFn(JSAny? v) => format!(v.dartify() as T).toJS; + final jsFormat = (format != null) ? jsFormatFn.toJS : null; + + _reactUseDebugValue(jsValue, jsFormat); +} diff --git a/packages/dart_node_react/lib/src/html_elements.dart b/packages/dart_node_react/lib/src/html_elements.dart new file mode 100644 index 0000000..dc94547 --- /dev/null +++ b/packages/dart_node_react/lib/src/html_elements.dart @@ -0,0 +1,620 @@ +/// HTML element factory functions for React. +/// +/// These functions create React elements for standard HTML tags. Each function +/// takes an optional props map, optional children list, and returns a +/// ReactElement. +library; + +import 'dart:js_interop'; + +import 'package:dart_node_react/src/react.dart'; + +// ============================================================================= +// Generic DOM Element Factory +// ============================================================================= + +/// Creates a React element for the given HTML tag name. +/// +/// This is a low-level function. Prefer using the specific element functions +/// like `div`, `span`, `p`, etc. +ReactElement domElement( + String tagName, [ + Map? props, + List? children, +]) => (children != null && children.isNotEmpty) + ? createElementWithChildren( + tagName.toJS, + props != null ? createProps(props) : null, + children, + ) + : createElement(tagName.toJS, props != null ? createProps(props) : null); + +// ============================================================================= +// Document Metadata +// ============================================================================= + +/// Creates a `` element. +ReactElement base([Map? props]) => + createElement('base'.toJS, props != null ? createProps(props) : null); + +/// Creates a `` element. +ReactElement head([ + Map? props, + List? children, +]) => domElement('head', props, children); + +/// Creates an `` element. +ReactElement htmlEl([ + Map? props, + List? children, +]) => domElement('html', props, children); + +/// Creates a `` element. +ReactElement link([Map? props]) => + createElement('link'.toJS, props != null ? createProps(props) : null); + +/// Creates a `` element. +ReactElement meta([Map? props]) => + createElement('meta'.toJS, props != null ? createProps(props) : null); + +/// Creates a `