Upgrade to Pro — share decks privately, control downloads, hide ads and more …

flutter_kaigi_2021.pdf

Kyohei Ito
November 30, 2021

 flutter_kaigi_2021.pdf

Kyohei Ito

November 30, 2021
Tweet

More Decks by Kyohei Ito

Other Decks in Programming

Transcript

  1. UIΧλϩάΞϓϦͰ࣮ݱ͢Δ


    Visual Regression Testing
    FlutterKaigi 2021/11/30

    View Slide

  2. About Me
    ҏ౻ ګฏ


    גࣜձࣾαΠόʔΤʔδΣϯτ


    Github: KyoheiG3


    Twitter: KyoheiG3

    View Slide

  3. • ֓ཁ


    • UI ΧλϩάΞϓϦ


    • ը໘Ωϟϓνϟ


    • ςετ
    ΞδΣϯμ

    View Slide

  4. • ֓ཁ


    • UI ΧλϩάΞϓϦ


    • ը໘Ωϟϓνϟ


    • ςετ
    ΞδΣϯμ

    View Slide

  5. • Unit Testing


    • UI Testing


    • Snapshot Testing


    • E2E Testing
    ςετͱ͸

    View Slide

  6. Golden Testing
    • ༧Ί༻ҙ͓͍ͯͨ͠Ϛελʔը૾ͱͷࠩ෼Λൺֱ͢Δςετ


    • ࠩ෼͕͋Ε͹ςετࣦഊ


    • UI ͷมߋ఺Λ໨ࢹͰ֬ೝ͢Δ΋ͷͰ͸ͳ͍

    View Slide

  7. Golden Testing
    void main() {


    testWidgets('Snapshot for MyApp', (tester) async {


    const app = MyApp();


    // ΩϟϓνϟαΠζΛઃఆ


    await tester.binding.setSurfaceSize(const Size(414, 896));


    await tester.pumpWidget(app);


    // Ϛελʔεφοϓγϣοτͱͷࠩ෼ൺֱ


    await expectLater(


    find.byWidget(app),


    matchesGoldenFile('MyApp.png'),


    );


    });


    }

    View Slide

  8. Golden Testing
    $ flutter test


    00:03 +1: All tests passed!

    View Slide

  9. Golden Testing
    mainAxisAlignment: MainAxisAlignment.center,


    children: [


    const Text(


    'You have pushed the button this many times:',


    ),


    Text(


    '$_counter',


    style: Theme.of(context).textTheme.headline4,


    ),


    ],
    ※ϓϩδΣΫτ࡞੒࣌ʹ࡞ΒΕΔίʔυ͔Βൈਮ

    View Slide

  10. Golden Testing
    mainAxisAlignment: MainAxisAlignment.start,


    children: [


    const Text(


    'You have pushed the button this many times:',


    ),


    Text(


    '$_counter',


    style: Theme.of(context).textTheme.headline4,


    ),


    ],
    ※ϓϩδΣΫτ࡞੒࣌ʹ࡞ΒΕΔίʔυ͔Βൈਮ
    DFOUFSΛTUBSUʹมߋͯ͠ςετͯ͠ΈΔ

    View Slide

  11. Golden Testing
    $ flutter test


    00:04 +0: Snapshot for MyApp


    00:04 +0 -1: Snapshot for MyApp [E]


    Test failed. See exception logs above.


    The test description was: Snapshot for MyApp


    The test description was: Snapshot for MyApp


    00:04 +0 -1: Some tests failed.

    View Slide

  12. Golden Testing
    ςετ࣮ߦը૾ ࠩ෼ը૾
    Ϛελʔը૾

    View Slide

  13. Golden Testing
    $ flutter test --update-goldens

    View Slide

  14. Visual Regression Testing
    • ը૾ճؼςετ


    • ը૾ͷࠩ෼Λݕग़͢Δεφοϓγϣοτςετͷ
    ͻͱͭ


    • UI ͕༧ظͤͣมߋ͞Ε͍ͯͳ͍͔Λ͔֬ΊΔͷ
    ʹඇৗʹ༗༻

    View Slide

  15. reg-suit

    View Slide

  16. reg-suit
    Blend Toggle
    Slide

    View Slide

  17. • reg-keygen-git-hash-plugin


    • reg-notify-github-plugin


    • reg-notify-slack-plugin


    • reg-publish-s3-plugin


    • reg-publish-gcs-plugin
    reg-suit

    View Slide

  18. reg-suit
    ࠩ෼ͷαϚϦʔΛ௨஌ͯ͘͠ΕΔ
    "QQSPWF͞ΕΔ·ͰϚʔδͰ͖ͳ͍

    View Slide

  19. reg-suit
    $ yarn add reg-suit


    $ yarn reg-suit init --use-yarn


    ? Plugin(s) to install (bold: recommended) (Press to select, to toggle all, to invert selection)


    ? Working directory of reg-suit. .reg


    ? Append ".reg" entry to your .gitignore file. Yes


    ? Directory contains actual images. catalog_app/test/screenshots


    ? Threshold, ranges from 0 to 1. Smaller value makes the comparison more sensitive. 0


    ? notify-github plugin requires a client ID of reg-suit GitHub app. Open installation window in your browser Yes


    ? This repositoriy's client ID of reg-suit GitHub app


    ? Create a new GCS bucket No


    ? Existing bucket name


    ? Update configuration file Yes


    ? Copy sample images to working dir No


    View Slide

  20. 🤔

    View Slide

  21. • ֓ཁ


    • UI ΧλϩάΞϓϦ


    • ը໘Ωϟϓνϟ


    • ςετ
    ΞδΣϯμ

    View Slide

  22. UI ΧλϩάΞϓϦ

    View Slide

  23. • playbook-ui (playbook-
    fl
    utter)


    • ݩʑฐࣾͷ OSS ͱͯ͠ଘࡏ͍ͯ͠Δ
    iOS ൛ͷ΋ͷΛ Flutter Խ
    UI ΧλϩάΞϓϦ

    View Slide

  24. • PlaybookGallery


    • Playbook


    • Story


    • Scenario
    playbook-ui (playbook-
    fl
    utter)

    View Slide

  25. playbook-ui (playbook-
    fl
    utter)
    @override


    Widget build(BuildContext context) {


    return MaterialApp(


    title: 'Playbook Demo',


    theme: ThemeData.light(),


    home: PlaybookGallery(


    title: 'Sample App',


    playbook: Playbook(stories: [


    Story('Sample Widget', scenarios: [


    Scenario(


    'Sample Scenario',


    child: Center(


    child: Container(


    width: 200,


    height: 250,


    color: Colors.amber,


    alignment: Alignment.center,


    child: const Text('Hello World'),


    ),


    ),


    ),


    ]),


    ]),


    ),


    );


    }

    View Slide

  26. playbook-ui (playbook-
    fl
    utter)
    @override


    Widget build(BuildContext context) {


    return MaterialApp(


    title: 'Playbook Demo',


    theme: ThemeData.light(),


    home: PlaybookGallery(


    title: 'Sample App',


    playbook: Playbook(stories: [


    Story('Sample Widget', scenarios: [


    Scenario(


    'Sample Scenario',


    child: Center(


    child: Container(


    width: 200,


    height: 250,


    color: Colors.amber,


    alignment: Alignment.center,


    child: const Text('Hello World'),


    ),


    ),


    ),


    ]),


    ]),


    ),


    );


    }
    Playbook ΠϯελϯεΛड͚औΓΧλϩάΞϓϦશମΛߏங͢Δ

    View Slide

  27. playbook-ui (playbook-
    fl
    utter)
    @override


    Widget build(BuildContext context) {


    return MaterialApp(


    title: 'Playbook Demo',


    theme: ThemeData.light(),


    home: PlaybookGallery(


    title: 'Sample App',


    playbook: Playbook(stories: [


    Story('Sample Widget', scenarios: [


    Scenario(


    'Sample Scenario',


    child: Center(


    child: Container(


    width: 200,


    height: 250,


    color: Colors.amber,


    alignment: Alignment.center,


    child: const Text('Hello World'),


    ),


    ),


    ),


    ]),


    ]),


    ),


    );


    }
    ड͚औͬͨ Story ෼ͷϦετ͕ PlaybookGallery ʹදࣔ͞ΕΔ

    View Slide

  28. playbook-ui (playbook-
    fl
    utter)
    @override


    Widget build(BuildContext context) {


    return MaterialApp(


    title: 'Playbook Demo',


    theme: ThemeData.light(),


    home: PlaybookGallery(


    title: 'Sample App',


    playbook: Playbook(stories: [


    Story('Sample Widget', scenarios: [


    Scenario(


    'Sample Scenario',


    child: Center(


    child: Container(


    width: 200,


    height: 250,


    color: Colors.amber,


    alignment: Alignment.center,


    child: const Text('Hello World'),


    ),


    ),


    ),


    ]),


    ]),


    ),


    );


    }
    ड͚औͬͨ Scenario ෼ͷϦετ͕ Story ͷ Row ʹදࣔ͞ΕΔ

    View Slide

  29. playbook-ui (playbook-
    fl
    utter)
    @override


    Widget build(BuildContext context) {


    return MaterialApp(


    title: 'Playbook Demo',


    theme: ThemeData.light(),


    home: PlaybookGallery(


    title: 'Sample App',


    playbook: Playbook(stories: [


    Story('Sample Widget', scenarios: [


    Scenario(


    'Sample Scenario',


    child: Center(


    child: Container(


    width: 200,


    height: 250,


    color: Colors.amber,


    alignment: Alignment.center,


    child: const Text('Hello World'),


    ),


    ),


    ),


    ]),


    ]),


    ),


    );


    }
    ड͚औͬͨ Widget ͷදࣔʹؔ͢Δ৘ใΛઃఆͰ͖Δ

    View Slide

  30. playbook-ui (playbook-
    fl
    utter)
    @override


    Widget build(BuildContext context) {


    return MaterialApp(


    title: 'Playbook Demo',


    theme: ThemeData.light(),


    home: PlaybookGallery(


    title: 'Sample App',


    playbook: Playbook(stories: [


    Story('Sample Widget', scenarios: [


    Scenario(


    'Sample Scenario',


    child: Center(


    child: Container(


    width: 200,


    height: 250,


    color: Colors.amber,


    alignment: Alignment.center,


    child: const Text('Hello World'),


    ),


    ),


    ),


    ]),


    ]),


    ),


    );


    }
    ಠࣗʹ࣮૷ͨ͠ Widget ͳͲΛ౉͢͜ͱͰදࣔ͞ΕΔ

    View Slide

  31. View Slide

  32. playbook-ui (playbook-
    fl
    utter)
    name: simple_app


    dependencies:


    flutter:


    sdk: flutter
    ϓϩμΫτΞϓϦߏ੒ྫ

    View Slide

  33. playbook-ui (playbook-
    fl
    utter)
    ΧλϩάΞϓϦΛ௥Ճ

    View Slide

  34. playbook-ui (playbook-
    fl
    utter)
    name: simple_catalog_app


    dependencies:


    flutter:


    sdk: flutter


    simple_app:


    path: ../simple_app


    playbook_ui:


    playbook:
    UI ΧλϩάΞϓϦߏ੒ྫ
    ϓϩμΫτͷΞϓϦΛґଘʹ௥Ճ

    View Slide

  35. ґଘΛݮΒ͢ํ๏
    playbook-ui (playbook-
    fl
    utter)

    View Slide

  36. playbook-ui (playbook-
    fl
    utter)
    ίϯϙʔωϯτΛ௥Ճ

    View Slide

  37. playbook-ui (playbook-
    fl
    utter)
    name: component


    dependencies:


    flutter:


    sdk: flutter
    UI ίϯϙʔωϯτύοέʔδԽྫ
    දࣔʹඞཁͳॲཧ͚ͩͷύοέʔδΛ࡞੒

    View Slide

  38. playbook-ui (playbook-
    fl
    utter)
    name: simple_app


    dependencies:


    flutter:


    sdk: flutter


    component:


    path: '../component'


    σόΠεઐ༻ͷґଘͳͲ΋ΞϓϦଆʹهड़͠ඞཁʹԠͯ͡ίϯϙʔωϯτʹ DI
    ࡞੒ͨ͠6*ίϯϙʔωϯτΛ௥Ճ

    View Slide

  39. playbook-ui (playbook-
    fl
    utter)
    name: simple_catalog_app


    dependencies:


    flutter:


    sdk: flutter


    component:


    path: '../component'


    playbook_ui:


    playbook:
    ಉ͘͡6*ίϯϙʔωϯτΛ௥Ճ
    දࣔʹඞཁͳίϯϙʔωϯτ͚ͩΛ import ͢Δ͜ͱͰϏϧυ͕࣌ؒ୹ॖ

    View Slide

  40. playbook-ui (playbook-
    fl
    utter)
    void main() {


    runApp(MyApp());


    }


    class MyApp extends StatelessWidget {


    @override


    Widget build(BuildContext context) {


    return MaterialApp(


    title: 'Playbook Demo',


    theme: ThemeData.light(),


    home: PlaybookGallery(


    title: 'Sample App',


    playbook: Playbook(stories: [


    Story('Sample Widget', scenarios: [


    Scenario(


    'Sample Scenario',


    child: ...,


    ),


    ])


    ]),


    ),


    );


    }


    }
    UI ΧλϩάΞϓϦ͕׬੒

    View Slide

  41. • ֓ཁ


    • UI ΧλϩάΞϓϦ


    • ը໘Ωϟϓνϟ


    • ςετ
    ΞδΣϯμ

    View Slide

  42. • ൺֱݩͷίϯϙʔωϯτͷΩϟϓνϟ


    • ൺֱର৅ͷίϯϙʔωϯτͷΩϟϓνϟ
    ը໘Ωϟϓνϟ

    View Slide


  43. fl
    utter_test matchesGoldenFile function


    • golden_toolkit (ebay)


    • playbook-snapshot (playbook-
    fl
    utter)
    ը໘Ωϟϓνϟ

    View Slide

  44. • playbook-snapshot (playbook-
    fl
    utter)


    • playbook-ui Ͱ࡞ͬͨίϯϙʔωϯτ
    ͷΩϟϓνϟΛαΫοͱࡱΕΔ
    ը໘Ωϟϓνϟ

    View Slide

  45. • Playbook extension


    • Snapshot
    playbook-snapshot (playbook-
    fl
    utter)

    View Slide

  46. Playbook(stories: [


    Story('Sample Widget', scenarios: [


    Scenario(


    'Sample Scenario',


    child: Center(


    child: Container(


    width: 200,


    height: 250,


    color: Colors.amber,


    alignment: Alignment.center,


    child: const Text('Hello World'),


    ),


    ),


    ),


    ])


    ]).run(


    Snapshot(


    directoryPath: 'screenshots',


    devices: [SnapshotDevice.iPhone8],


    ),


    (widget) {


    return MaterialApp(


    debugShowCheckedModeBanner: false,


    home: Material(child: widget),


    );


    },


    );
    playbook-snapshot (playbook-
    fl
    utter)

    View Slide

  47. Playbook(stories: [


    Story('Sample Widget', scenarios: [


    Scenario(


    'Sample Scenario',


    child: Center(


    child: Container(


    width: 200,


    height: 250,


    color: Colors.amber,


    alignment: Alignment.center,


    child: const Text('Hello World'),


    ),


    ),


    ),


    ])


    ]).run(


    Snapshot(


    directoryPath: 'screenshots',


    devices: [SnapshotDevice.iPhone8],


    ),


    (widget) {


    return MaterialApp(


    debugShowCheckedModeBanner: false,


    home: Material(child: widget),


    );


    },


    );
    playbook-snapshot (playbook-
    fl
    utter)
    1MBZCPPLʹSVOϝιου͕௥Ճ͞ΕΔ

    View Slide

  48. Playbook(stories: [


    Story('Sample Widget', scenarios: [


    Scenario(


    'Sample Scenario',


    child: Center(


    child: Container(


    width: 200,


    height: 250,


    color: Colors.amber,


    alignment: Alignment.center,


    child: const Text('Hello World'),


    ),


    ),


    ),


    ])


    ]).run(


    Snapshot(


    directoryPath: 'screenshots',


    devices: [SnapshotDevice.iPhone8],


    ),


    (widget) {


    return MaterialApp(


    debugShowCheckedModeBanner: false,


    home: Material(child: widget),


    );


    },


    );
    playbook-snapshot (playbook-
    fl
    utter)
    4DFOBSJP͕͍࣋ͬͯΔXJEHFU͕౉͞ΕΔ
    ΩϟϓνϟΛࡱΓ͍ͨ8JEHFU

    View Slide

  49. Playbook(stories: [


    Story('Sample Widget', scenarios: [


    Scenario(


    'Sample Scenario',


    child: Center(


    child: Container(


    width: 200,


    height: 250,


    color: Colors.amber,


    alignment: Alignment.center,


    child: const Text('Hello World'),


    ),


    ),


    ),


    ])


    ]).run(


    Snapshot(


    directoryPath: 'screenshots',


    devices: [SnapshotDevice.iPhone8],


    ),


    (widget) {


    return MaterialApp(


    debugShowCheckedModeBanner: false,


    home: Material(child: widget),


    );


    },


    );
    playbook-snapshot (playbook-
    fl
    utter)
    Snapshot ઃఆྫ

    View Slide

  50. Future main() async {


    await Playbook(stories: [


    Story('Sample Widget', scenarios: [


    Scenario(


    'Sample Scenario',


    child: Center(


    child: Container(


    width: 200,


    height: 250,


    color: Colors.amber,


    alignment: Alignment.center,


    child: const Text('Hello World'),


    ),


    ),


    ),


    ])


    ]).run(


    Snapshot(


    directoryPath: 'screenshots',


    devices: [SnapshotDevice.iPhone8],


    ),


    (widget) {


    return MaterialApp(


    debugShowCheckedModeBanner: false,


    home: Material(child: widget),


    );


    },


    );


    }
    playbook-snapshot (playbook-
    fl
    utter)

    View Slide

  51. Future main() async {


    await Playbook(stories: [


    Story('Sample Widget', scenarios: [


    Scenario(


    'Sample Scenario',


    child: Center(


    child: Container(


    width: 200,


    height: 250,


    color: Colors.amber,


    alignment: Alignment.center,


    child: const Text('Hello World'),


    ),


    ),


    ),


    ])


    ]).run(


    Snapshot(


    directoryPath: 'screenshots',


    devices: [SnapshotDevice.iPhone8],


    ),


    (widget) {


    return MaterialApp(


    debugShowCheckedModeBanner: false,


    home: Material(child: widget),


    );


    },


    );


    }
    playbook-snapshot (playbook-
    fl
    utter)
    ͜ͷ෦෼͸ΞϓϦ΋ςετ΋ڞ௨

    View Slide

  52. Playbook playbook() {


    return Playbook(stories: [


    Story('Sample Widget', scenarios: [


    Scenario(


    'Sample Scenario',


    child: Center(


    child: Container(


    width: 200,


    height: 250,


    color: Colors.amber,


    alignment: Alignment.center,


    child: const Text('Hello World'),


    ),


    ),


    ),


    ])


    ]);


    }
    playbook-snapshot (playbook-
    fl
    utter)

    View Slide

  53. void main() {


    runApp(MyApp());


    }


    class MyApp extends StatelessWidget {


    @override


    Widget build(BuildContext context) {


    return MaterialApp(


    title: 'Playbook Demo',


    theme: ThemeData.light(),


    home: PlaybookGallery(


    title: 'Sample App',


    playbook: playbook(),


    ),


    );


    }


    }
    playbook-snapshot (playbook-
    fl
    utter)

    View Slide

  54. Future main() async {


    await playbook().run(


    Snapshot(


    directoryPath: 'screenshots',


    devices: [SnapshotDevice.iPhone8],


    ),


    (widget) {


    return MaterialApp(


    debugShowCheckedModeBanner: false,


    home: Material(child: widget),


    );


    },


    );


    }
    playbook-snapshot (playbook-
    fl
    utter)

    View Slide

  55. • தͰ͸
    fl
    utter_test ͷ matchesGoldenFile Λ࣮ߦ͍ͯ͠Δ


    • ϑΥϯτ΍ΞΠίϯ͕ಡΈࠐΊͳ͍໰୊ͷαϙʔτ
    playbook-snapshot (playbook-
    fl
    utter)
    ৄ͘͠͸

    https://speakerdeck.com/tomokitakahashi/shi-jian-
    fl
    utter-visual-regression-testing

    View Slide

  56. name: simple_catalog_app


    flutter:


    fonts:


    - family: Roboto


    fonts:


    - asset: fonts/Roboto-Regular.ttf
    playbook-snapshot (playbook-
    fl
    utter)
    Ωϟϓνϟ࣌ͷϑΥϯτΛ௥Ճ͢Δ

    View Slide

  57. • ඇಉظॲཧ


    • ௨৴σʔλʢը૾ʣ


    • ࣌ؒ


    • Ξχϝʔγϣϯ


    • etc…
    DI

    View Slide

  58. Future main() async {


    await playbook().run(


    Snapshot(


    directoryPath: 'screenshots',


    devices: [SnapshotDevice.iPhone8],


    ),


    (widget) {


    return ProviderScope(


    overrides: [],


    MaterialApp(


    debugShowCheckedModeBanner: false,


    home: Material(child: widget),


    ),


    );


    },


    );


    }
    DI
    ͜ͷ͋ͨΓͰ%*Մೳ

    View Slide

  59. • Story ࡞੒ͷ؆ུԽ


    • Scenario ࡞੒༻ͷΞϊςʔγϣϯ


    • Playbook ͷΠϯελϯεࣗಈੜ੒


    • build_runner Ͱ࣮ߦՄೳ
    playbook-generator (playbook-
    fl
    utter)

    View Slide

  60. Playbook playbook() {


    return Playbook(stories: [


    Story('Sample Widget', scenarios: [


    Scenario(


    'Sample Scenario',


    child: Center(


    child: Container(


    width: 200,


    height: 250,


    color: Colors.amber,


    alignment: Alignment.center,


    child: const Text('Hello World'),


    ),


    ),


    ),


    ])


    ]);


    }
    playbook-generator (playbook-
    fl
    utter)
    Before

    View Slide

  61. const storyTitle = 'Sample Widget';


    @GenerateScenario(title: 'Sample Scenario')


    Widget sampleScenario() => Center(


    child: Container(


    width: 200,


    height: 250,


    color: Colors.amber,


    alignment: Alignment.center,


    child: const Text('Hello World'),


    ),


    );
    playbook-generator (playbook-
    fl
    utter)
    After

    View Slide

  62. void main() {


    runApp(MyApp());


    }


    class MyApp extends StatelessWidget {


    @override


    Widget build(BuildContext context) {


    return MaterialApp(


    title: 'Playbook Demo',


    theme: ThemeData.light(),


    home: PlaybookGallery(


    title: 'Sample App',


    playbook: playbook,


    ),


    );


    }


    }
    playbook-generator (playbook-
    fl
    utter)
    ม਺ HFUUFS
    ͕ࣗಈੜ੒͞Ε͍ͯΔ

    View Slide

  63. Future main() async {


    await playbook.run(


    Snapshot(


    directoryPath: 'screenshots',


    devices: [SnapshotDevice.iPhone8],


    ),


    (widget) {


    return MaterialApp(


    debugShowCheckedModeBanner: false,


    home: Material(child: widget),


    );


    },


    );


    }
    playbook-generator (playbook-
    fl
    utter)
    ม਺ HFUUFS
    ͕ࣗಈੜ੒͞Ε͍ͯΔ

    View Slide

  64. • ֓ཁ


    • UI ΧλϩάΞϓϦ


    • ը໘Ωϟϓνϟ


    • ςετ
    ΞδΣϯμ

    View Slide

  65. • PR ࣌ʹ VRT ͕࣮ߦ͞ΕΔΑ͏ʹ CI Ͱઃఆ͢Δ


    • ௨஌ܥͷϓϥάΠϯΛ࢖͑͹ςετ݁ՌΛ௨஌ͯ͘͠ΕΔ


    • ϨϏϡʔޙʹ Approve ͢Δ·ͰϚʔδͰ͖ͳͨ͘͠ΓͰ͖Δ
    ςετ

    View Slide

  66. {


    "dependencies": {


    "reg-suit": "^0.11.1"


    },


    "devDependencies": {


    "reg-keygen-git-hash-plugin": "^0.11.1",


    "reg-notify-github-plugin": "^0.11.1",


    "reg-publish-gcs-plugin": "^0.11.1"


    },


    "scripts": {


    "regression": "reg-suit run"


    }


    }
    CI
    SFHTVJUΛ࣮ߦ͢ΔͨΊͷઃఆΛ௥Ճ

    View Slide

  67. version: 2.1


    orbs:


    android: circleci/[email protected]


    flutter: circleci/[email protected]


    node: circleci/[email protected]


    jobs:


    vrt:


    executor: android/android


    steps:


    - checkout


    - node/install:


    install-yarn: true


    - flutter/install_sdk:


    - run:


    name: Install melos


    command: |


    dart pub global activate melos


    melos run pub:get


    - run:


    name: Run Golden Testing


    command: melos run test:snapshot


    - run:


    name: Install dependencies


    command: yarn


    - run:


    name: Run VRT


    command: yarn regression


    workflows:


    vrt:


    jobs:


    - vrt
    CI

    View Slide

  68. @override


    Widget build(BuildContext context) {


    return Container(


    color: Colors.amberAccent,


    child: Row(


    mainAxisAlignment: MainAxisAlignment.center,


    crossAxisAlignment: CrossAxisAlignment.center,


    children: [


    Icon(Icons.star),


    SizedBox(width: 16),


    Text(text, style: Theme.of(context).textTheme.headline5)


    ],


    ),


    );


    }
    ςετ

    View Slide

  69. @override


    Widget build(BuildContext context) {


    return Container(


    color: Colors.amberAccent,


    child: Row(


    mainAxisAlignment: MainAxisAlignment.start,


    crossAxisAlignment: CrossAxisAlignment.center,


    children: [


    Icon(Icons.star),


    SizedBox(width: 16),


    Text(text, style: Theme.of(context).textTheme.headline5)


    ],


    ),


    );


    }
    ςετ
    ෆ۩߹͔൑அͮ͠Β͍

    View Slide

  70. • ςετࣗମΛॻ࣌ؒ͘͸ͱʹ͔͘ݮΒ͍ͨ͠


    • playbook_ui Λར༻ͯ͠ΩϟϓνϟͷࣗಈԽ


    • ࡞ͬͯյͯ͠Λ΋ͬͱؾܰʹߦ͑ΔΑ͏ʹͳΓ·͢ʂ
    ςετ

    View Slide

  71. • https://github.com/playbook-ui/playbook-
    fl
    utter


    • https://github.com/Dropsource/monarch


    • https://github.com/widgetbook/widgetbook


    • https://github.com/ookami-kb/storybook_
    fl
    utter


    • https://github.com/eBay/
    fl
    utter_glove_box/tree/master/packages/
    golden_toolkit
    ࢀߟ URL

    View Slide

  72. Thanks !

    View Slide