// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/web_template.dart'; import '../src/common.dart'; import '../src/context.dart'; const htmlSample1 = '''
'''; const htmlSample2 = '''
'''; const htmlSampleInlineFlutterJsBootstrap = '''
'''; const htmlSampleInlineFlutterJsBootstrapOutput = '''
'''; const htmlSampleFullFlutterBootstrapReplacement = '''
'''; const htmlSampleFullFlutterBootstrapReplacementOutput = '''
'''; const htmlSampleLegacyVar = '''
'''; const htmlSampleLegacyLoadEntrypoint = '''
'''; String htmlSample2Replaced({required String baseHref, required String serviceWorkerVersion}) => '''
'''; const htmlSample3 = '''
'''; const htmlSampleStaticAssetsUrl = '''
'''; String htmlSampleStaticAssetsUrlReplaced({required String staticAssetsUrl}) => '''
'''; void main() { final fs = MemoryFileSystem(); final logger = BufferLogger.test(); final File flutterJs = fs.file('flutter.js'); flutterJs.writeAsStringSync('(flutter.js content)'); test('can parse baseHref', () { expect(WebTemplate.baseHref(''), 'foo/111'); expect(WebTemplate.baseHref(htmlSample1), 'foo/222'); expect(WebTemplate.baseHref(htmlSample2), ''); // Placeholder base href. }); test('handles missing baseHref', () { expect(WebTemplate.baseHref(''), ''); expect(WebTemplate.baseHref(''), ''); expect(WebTemplate.baseHref(htmlSample3), ''); }); test('throws on invalid baseHref', () { expect(() => WebTemplate.baseHref(''), throwsToolExit()); expect(() => WebTemplate.baseHref(''), throwsToolExit()); expect(() => WebTemplate.baseHref(''), throwsToolExit()); expect(() => WebTemplate.baseHref(''), throwsToolExit()); expect(() => WebTemplate.baseHref(''), throwsToolExit()); }); test('applies substitutions', () { const indexHtml = WebTemplate(htmlSample2); expect( indexHtml.withSubstitutions( baseHref: '/foo/333/', serviceWorkerVersion: 'v123xyz', flutterJsFile: flutterJs, logger: logger, ), htmlSample2Replaced(baseHref: '/foo/333/', serviceWorkerVersion: 'v123xyz'), ); }); test('applies substitutions with legacy var version syntax', () { const indexHtml = WebTemplate(htmlSampleLegacyVar); expect( indexHtml.withSubstitutions( baseHref: '/foo/333/', serviceWorkerVersion: 'v123xyz', flutterJsFile: flutterJs, logger: logger, ), htmlSample2Replaced(baseHref: '/foo/333/', serviceWorkerVersion: 'v123xyz'), ); }); test('applies substitutions to inline flutter.js bootstrap script', () { const indexHtml = WebTemplate(htmlSampleInlineFlutterJsBootstrap); expect(indexHtml.getWarnings(), isEmpty); expect( indexHtml.withSubstitutions( baseHref: '/', serviceWorkerVersion: '(service worker version)', flutterJsFile: flutterJs, buildConfig: '(build config)', logger: logger, ), htmlSampleInlineFlutterJsBootstrapOutput, ); }); test('applies substitutions to full flutter_bootstrap.js replacement', () { const indexHtml = WebTemplate(htmlSampleFullFlutterBootstrapReplacement); expect(indexHtml.getWarnings(), isEmpty); expect( indexHtml.withSubstitutions( baseHref: '/', serviceWorkerVersion: '(service worker version)', flutterJsFile: flutterJs, buildConfig: '(build config)', flutterBootstrapJs: '(flutter bootstrap script)', logger: logger, ), htmlSampleFullFlutterBootstrapReplacementOutput, ); }); test('applies substitutions to static assets url', () { const indexHtml = WebTemplate(htmlSampleStaticAssetsUrl); const expectedStaticAssetsUrl = 'https://static.example.com/my-app/'; expect( indexHtml.withSubstitutions( baseHref: '/', serviceWorkerVersion: 'v123xyz', flutterJsFile: flutterJs, staticAssetsUrl: expectedStaticAssetsUrl, logger: logger, ), htmlSampleStaticAssetsUrlReplaced(staticAssetsUrl: expectedStaticAssetsUrl), ); }); test('re-parses after substitutions', () { const indexHtml = WebTemplate(htmlSample2); expect(WebTemplate.baseHref(htmlSample2), ''); // Placeholder base href. final String substituted = indexHtml.withSubstitutions( baseHref: '/foo/333/', serviceWorkerVersion: 'v123xyz', flutterJsFile: flutterJs, logger: logger, ); // The parsed base href should be updated after substitutions. expect(WebTemplate.baseHref(substituted), 'foo/333'); }); test('warns on legacy service worker patterns', () { const indexHtml = WebTemplate(htmlSampleLegacyVar); final List warnings = indexHtml.getWarnings(); expect(warnings, hasLength(2)); final Iterable serviceWorkerWarnings = warnings.where( (WebTemplateWarning warning) => warning.lineNumber == 13 || warning.lineNumber == 16, ); expect(serviceWorkerWarnings, hasLength(2)); expect( serviceWorkerWarnings, everyElement( isA().having( (WebTemplateWarning warning) => warning.warningText, 'service worker warning message', contains( "Flutter's service worker is deprecated and will be removed in a future Flutter release.", ), ), ), ); }); test('warns on legacy FlutterLoader.loadEntrypoint', () { const indexHtml = WebTemplate(htmlSampleLegacyLoadEntrypoint); final List warnings = indexHtml.getWarnings(); expect(warnings.length, 1); expect(warnings.single.lineNumber, 14); }); test('applies web-define variable substitutions', () { const htmlWithWebDefines = ''' Test '''; const indexHtml = WebTemplate(htmlWithWebDefines); final String result = indexHtml.withSubstitutions( baseHref: '/', serviceWorkerVersion: null, flutterJsFile: flutterJs, webDefines: { 'API_URL': 'https://api.example.com', 'ENV': 'production', 'DEBUG_MODE': 'false', }, logger: logger, ); expect(result, contains("apiUrl: 'https://api.example.com'")); expect(result, contains("environment: 'production'")); expect(result, contains('debugMode: false')); }); testUsingContext('logs warning when user defined web-define variable is missing', () { const htmlWithMissingVar = ''' Test '''; const indexHtml = WebTemplate(htmlWithMissingVar); final String result = indexHtml.withSubstitutions( baseHref: '/', serviceWorkerVersion: null, flutterJsFile: flutterJs, webDefines: {}, // Missing API_URL logger: testLogger, ); expect(testLogger.warningText, contains('Missing web-define variable: API_URL')); // Verify the placeholder is preserved expect(result, contains("const apiUrl = '{{API_URL}}';")); }); testUsingContext('logs warning with multiple missing user defined variables', () { const htmlWithMultipleMissingVars = ''' Test '''; const indexHtml = WebTemplate(htmlWithMultipleMissingVars); final String result = indexHtml.withSubstitutions( baseHref: '/', serviceWorkerVersion: null, flutterJsFile: flutterJs, webDefines: {'API_URL': 'test'}, // Missing ENV, VERSION logger: testLogger, ); expect(testLogger.warningText, contains('Missing web-define variables: ENV, VERSION')); expect(result, contains("env: '{{ENV}}'")); expect(result, contains("version: '{{VERSION}}'")); }); testUsingContext( 'ignores Flutter built-in variables and logs warning for missing user variables', () { const htmlWithBuiltInVars = ''' Test '''; const indexHtml = WebTemplate(htmlWithBuiltInVars); final String result = indexHtml.withSubstitutions( baseHref: '/', serviceWorkerVersion: null, flutterJsFile: flutterJs, buildConfig: 'test config', webDefines: {}, // Missing CUSTOM_VAR but built-in vars should be ignored logger: testLogger, ); expect(testLogger.warningText, contains('Missing web-define variable: CUSTOM_VAR')); expect(result, contains("const customVar = '{{CUSTOM_VAR}}';")); }, ); test('allows empty web-define variables', () { const htmlWithEmptyVar = ''' Test '''; const indexHtml = WebTemplate(htmlWithEmptyVar); final String result = indexHtml.withSubstitutions( baseHref: '/', serviceWorkerVersion: null, flutterJsFile: flutterJs, webDefines: {'EMPTY_VAR': ''}, logger: logger, ); expect(result, contains("const value = '';")); }); }