Ensure precache --web works on dev branch (#42289)

diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart
index 889ff74..07c1b73 100644
--- a/packages/flutter_tools/lib/src/cache.dart
+++ b/packages/flutter_tools/lib/src/cache.dart
@@ -16,21 +16,25 @@
 import 'base/os.dart';
 import 'base/platform.dart';
 import 'base/process.dart';
+import 'features.dart';
 import 'globals.dart';
 
 /// A tag for a set of development artifacts that need to be cached.
 class DevelopmentArtifact {
 
-  const DevelopmentArtifact._(this.name, {this.unstable = false});
+  const DevelopmentArtifact._(this.name, {this.unstable = false, this.feature});
 
   /// The name of the artifact.
   ///
   /// This should match the flag name in precache.dart
   final String name;
 
-  /// Whether this artifact should be unavailable on stable branches.
+  /// Whether this artifact should be unavailable on master branch only.
   final bool unstable;
 
+  /// A feature to control the visibility of this artifact.
+  final Feature feature;
+
   /// Artifacts required for Android development.
   static const DevelopmentArtifact androidGenSnapshot = DevelopmentArtifact._('android_gen_snapshot');
   static const DevelopmentArtifact androidMaven = DevelopmentArtifact._('android_maven');
@@ -41,7 +45,7 @@
   static const DevelopmentArtifact iOS = DevelopmentArtifact._('ios');
 
   /// Artifacts required for web development.
-  static const DevelopmentArtifact web = DevelopmentArtifact._('web', unstable: true);
+  static const DevelopmentArtifact web = DevelopmentArtifact._('web', feature: flutterWebFeature);
 
   /// Artifacts required for desktop macOS.
   static const DevelopmentArtifact macOS = DevelopmentArtifact._('macos', unstable: true);
@@ -75,6 +79,9 @@
     universal,
     flutterRunner,
   ];
+
+  @override
+  String toString() => 'Artifact($name, $unstable)';
 }
 
 /// A wrapper around the `bin/cache/` directory.
diff --git a/packages/flutter_tools/lib/src/commands/precache.dart b/packages/flutter_tools/lib/src/commands/precache.dart
index e7f7103..d8ccd3a 100644
--- a/packages/flutter_tools/lib/src/commands/precache.dart
+++ b/packages/flutter_tools/lib/src/commands/precache.dart
@@ -5,6 +5,7 @@
 import 'dart:async';
 
 import '../cache.dart';
+import '../features.dart';
 import '../globals.dart';
 import '../runner/flutter_command.dart';
 import '../version.dart';
@@ -65,6 +66,9 @@
       if (!FlutterVersion.instance.isMaster && artifact.unstable) {
         continue;
       }
+      if (artifact.feature != null && !featureFlags.isEnabled(artifact.feature)) {
+        continue;
+      }
       if (argResults[artifact.name]) {
         requiredArtifacts.add(artifact);
       }
diff --git a/packages/flutter_tools/lib/src/features.dart b/packages/flutter_tools/lib/src/features.dart
index b0d979c..332506d 100644
--- a/packages/flutter_tools/lib/src/features.dart
+++ b/packages/flutter_tools/lib/src/features.dart
@@ -17,7 +17,7 @@
 /// The interface used to determine if a particular [Feature] is enabled.
 ///
 /// The rest of the tools code should use this class instead of looking up
-/// features directly. To faciliate rolls to google3 and other clients, all
+/// features directly. To facilitate rolls to google3 and other clients, all
 /// flags should be provided with a default implementation here. Clients that
 /// use this class should extent instead of implement, so that new flags are
 /// picked up automatically.
@@ -25,22 +25,24 @@
   const FeatureFlags();
 
   /// Whether flutter desktop for linux is enabled.
-  bool get isLinuxEnabled => _isEnabled(flutterLinuxDesktopFeature);
+  bool get isLinuxEnabled => isEnabled(flutterLinuxDesktopFeature);
 
   /// Whether flutter desktop for macOS is enabled.
-  bool get isMacOSEnabled => _isEnabled(flutterMacOSDesktopFeature);
+  bool get isMacOSEnabled => isEnabled(flutterMacOSDesktopFeature);
 
   /// Whether flutter web is enabled.
-  bool get isWebEnabled => _isEnabled(flutterWebFeature);
+  bool get isWebEnabled => isEnabled(flutterWebFeature);
 
   /// Whether flutter desktop for Windows is enabled.
-  bool get isWindowsEnabled => _isEnabled(flutterWindowsDesktopFeature);
+  bool get isWindowsEnabled => isEnabled(flutterWindowsDesktopFeature);
 
   /// Whether the new Android embedding is enabled.
-  bool get isNewAndroidEmbeddingEnabled => _isEnabled(flutterNewAndroidEmbeddingFeature);
+  bool get isNewAndroidEmbeddingEnabled => isEnabled(flutterNewAndroidEmbeddingFeature);
 
-  // Calculate whether a particular feature is enabled for the current channel.
-  static bool _isEnabled(Feature feature) {
+  /// Whether a particular feature is enabled for the current channel.
+  ///
+  /// Prefer using one of the specific getters above instead of this API.
+  bool isEnabled(Feature feature) {
     final String currentChannel = FlutterVersion.instance.channel;
     final FeatureChannelSetting featureSetting = feature.getSettingForChannel(currentChannel);
     if (!featureSetting.available) {
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/precache_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/precache_test.dart
index 128662e..3794ff2 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/precache_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/precache_test.dart
@@ -4,6 +4,7 @@
 
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/precache.dart';
+import 'package:flutter_tools/src/features.dart';
 import 'package:flutter_tools/src/runner/flutter_command.dart';
 import 'package:flutter_tools/src/version.dart';
 import 'package:mockito/mockito.dart';
@@ -11,151 +12,183 @@
 import '../../src/common.dart';
 import '../../src/context.dart';
 import '../../src/mocks.dart';
+import '../../src/testbed.dart';
 
 void main() {
-  group('precache', () {
-    final MockCache cache = MockCache();
-    Set<DevelopmentArtifact> artifacts;
+  MockCache cache;
+  Set<DevelopmentArtifact> artifacts;
+  MockFlutterVersion flutterVersion;
+  MockFlutterVersion masterFlutterVersion;
+
+  setUp(() {
+    cache = MockCache();
+    // Release lock between test cases.
+    Cache.releaseLockEarly();
 
     when(cache.isUpToDate()).thenReturn(false);
     when(cache.updateAll(any)).thenAnswer((Invocation invocation) {
       artifacts = invocation.positionalArguments.first;
       return Future<void>.value(null);
     });
-
-    testUsingContext('Adds artifact flags to requested artifacts', () async {
-      final PrecacheCommand command = PrecacheCommand();
-      applyMocksToCommand(command);
-      await createTestCommandRunner(command).run(
-        const <String>[
-          'precache',
-          '--ios',
-          '--android',
-          '--web',
-          '--macos',
-          '--linux',
-          '--windows',
-          '--fuchsia',
-          '--flutter_runner',
-        ],
-      );
-      expect(artifacts, unorderedEquals(<DevelopmentArtifact>{
-        DevelopmentArtifact.universal,
-        DevelopmentArtifact.iOS,
-        DevelopmentArtifact.androidGenSnapshot,
-        DevelopmentArtifact.androidMaven,
-        DevelopmentArtifact.androidInternalBuild,
-        DevelopmentArtifact.web,
-        DevelopmentArtifact.macOS,
-        DevelopmentArtifact.linux,
-        DevelopmentArtifact.windows,
-        DevelopmentArtifact.fuchsia,
-        DevelopmentArtifact.flutterRunner,
-      }));
-    }, overrides: <Type, Generator>{
-      Cache: () => cache,
-    });
-
-    testUsingContext('Expands android artifacts when the android flag is used', () async {
-      // Release lock between test cases.
-      Cache.releaseLockEarly();
-
-      final PrecacheCommand command = PrecacheCommand();
-      applyMocksToCommand(command);
-      await createTestCommandRunner(command).run(
-        const <String>[
-          'precache',
-          '--no-ios',
-          '--android',
-        ],
-      );
-      expect(artifacts, unorderedEquals(<DevelopmentArtifact>{
-        DevelopmentArtifact.universal,
-        DevelopmentArtifact.androidGenSnapshot,
-        DevelopmentArtifact.androidMaven,
-        DevelopmentArtifact.androidInternalBuild,
-      }));
-    }, overrides: <Type, Generator>{
-      Cache: () => cache,
-    });
-
-    testUsingContext('Adds artifact flags to requested android artifacts', () async {
-      // Release lock between test cases.
-      Cache.releaseLockEarly();
-
-      final PrecacheCommand command = PrecacheCommand();
-      applyMocksToCommand(command);
-      await createTestCommandRunner(command).run(
-        const <String>[
-          'precache',
-          '--no-ios',
-          '--android_gen_snapshot',
-          '--android_maven',
-          '--android_internal_build',
-        ],
-      );
-      expect(artifacts, unorderedEquals(<DevelopmentArtifact>{
-        DevelopmentArtifact.universal,
-        DevelopmentArtifact.androidGenSnapshot,
-        DevelopmentArtifact.androidMaven,
-        DevelopmentArtifact.androidInternalBuild,
-      }));
-    }, overrides: <Type, Generator>{
-      Cache: () => cache,
-    });
-
-    final MockFlutterVersion flutterVersion = MockFlutterVersion();
+    flutterVersion = MockFlutterVersion();
     when(flutterVersion.isMaster).thenReturn(false);
+    masterFlutterVersion = MockFlutterVersion();
+    when(masterFlutterVersion.isMaster).thenReturn(true);
+  });
 
-    testUsingContext('Adds artifact flags to requested artifacts on stable', () async {
-      // Release lock between test cases.
-      Cache.releaseLockEarly();
-      final PrecacheCommand command = PrecacheCommand();
-      applyMocksToCommand(command);
-      await createTestCommandRunner(command).run(
-        const <String>[
-          'precache',
-          '--ios',
-          '--android_gen_snapshot',
-          '--android_maven',
-          '--android_internal_build',
-          '--web',
-          '--macos',
-          '--linux',
-          '--windows',
-          '--fuchsia',
-          '--flutter_runner',
-        ],
-      );
-      expect(artifacts, unorderedEquals(<DevelopmentArtifact>{
-        DevelopmentArtifact.universal,
-        DevelopmentArtifact.iOS,
-        DevelopmentArtifact.androidGenSnapshot,
-        DevelopmentArtifact.androidMaven,
-        DevelopmentArtifact.androidInternalBuild,
-      }));
-    }, overrides: <Type, Generator>{
-      Cache: () => cache,
-      FlutterVersion: () => flutterVersion,
-    });
-    testUsingContext('Downloads artifacts when --force is provided', () async {
-      when(cache.isUpToDate()).thenReturn(true);
-      // Release lock between test cases.
-      Cache.releaseLockEarly();
-      final PrecacheCommand command = PrecacheCommand();
-      applyMocksToCommand(command);
-      await createTestCommandRunner(command).run(const <String>['precache', '--force']);
-      expect(artifacts, unorderedEquals(<DevelopmentArtifact>{
-        DevelopmentArtifact.universal,
-        DevelopmentArtifact.iOS,
-        DevelopmentArtifact.androidGenSnapshot,
-        DevelopmentArtifact.androidMaven,
-        DevelopmentArtifact.androidInternalBuild,
-      }));
-    }, overrides: <Type, Generator>{
-      Cache: () => cache,
-      FlutterVersion: () => flutterVersion,
-    });
+  testUsingContext('precache downloads web artifacts on dev branch when feature is enabled.', () async {
+    final PrecacheCommand command = PrecacheCommand();
+    applyMocksToCommand(command);
+    await createTestCommandRunner(command).run(const <String>['precache', '--web', '--no-android', '--no-ios']);
+
+    expect(artifacts, unorderedEquals(<DevelopmentArtifact>{
+      DevelopmentArtifact.universal,
+      DevelopmentArtifact.web,
+      DevelopmentArtifact.androidGenSnapshot,
+      DevelopmentArtifact.androidMaven,
+    }));
+  }, overrides: <Type, Generator>{
+    Cache: () => cache,
+    FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
+  });
+
+  testUsingContext('precache does not download web artifacts on dev branch when feature is enabled.', () async {
+    final PrecacheCommand command = PrecacheCommand();
+    applyMocksToCommand(command);
+    await createTestCommandRunner(command).run(const <String>['precache', '--web', '--no-android', '--no-ios']);
+
+    expect(artifacts, unorderedEquals(<DevelopmentArtifact>{
+      DevelopmentArtifact.universal,
+      DevelopmentArtifact.androidGenSnapshot,
+      DevelopmentArtifact.androidMaven,
+    }));
+  }, overrides: <Type, Generator>{
+    Cache: () => cache,
+    FeatureFlags: () => TestFeatureFlags(isWebEnabled: false),
+  });
+
+  testUsingContext('precache adds artifact flags to requested artifacts', () async {
+    final PrecacheCommand command = PrecacheCommand();
+    applyMocksToCommand(command);
+    await createTestCommandRunner(command).run(
+      const <String>[
+        'precache',
+        '--ios',
+        '--android',
+        '--web',
+        '--macos',
+        '--linux',
+        '--windows',
+        '--fuchsia',
+        '--flutter_runner',
+      ],
+    );
+    expect(artifacts, unorderedEquals(<DevelopmentArtifact>{
+      DevelopmentArtifact.universal,
+      DevelopmentArtifact.iOS,
+      DevelopmentArtifact.androidGenSnapshot,
+      DevelopmentArtifact.androidMaven,
+      DevelopmentArtifact.androidInternalBuild,
+      DevelopmentArtifact.web,
+      DevelopmentArtifact.macOS,
+      DevelopmentArtifact.linux,
+      DevelopmentArtifact.windows,
+      DevelopmentArtifact.fuchsia,
+      DevelopmentArtifact.flutterRunner,
+    }));
+  }, overrides: <Type, Generator>{
+    Cache: () => cache,
+    FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
+    FlutterVersion: () => masterFlutterVersion,
+  });
+
+  testUsingContext('precache expands android artifacts when the android flag is used', () async {
+    final PrecacheCommand command = PrecacheCommand();
+    applyMocksToCommand(command);
+    await createTestCommandRunner(command).run(
+      const <String>[
+        'precache',
+        '--no-ios',
+        '--android',
+      ],
+    );
+    expect(artifacts, unorderedEquals(<DevelopmentArtifact>{
+      DevelopmentArtifact.universal,
+      DevelopmentArtifact.androidGenSnapshot,
+      DevelopmentArtifact.androidMaven,
+      DevelopmentArtifact.androidInternalBuild,
+    }));
+  }, overrides: <Type, Generator>{
+    Cache: () => cache,
+  });
+
+  testUsingContext('precache adds artifact flags to requested android artifacts', () async {
+    final PrecacheCommand command = PrecacheCommand();
+    applyMocksToCommand(command);
+    await createTestCommandRunner(command).run(
+      const <String>[
+        'precache',
+        '--no-ios',
+        '--android_gen_snapshot',
+        '--android_maven',
+        '--android_internal_build',
+      ],
+    );
+    expect(artifacts, unorderedEquals(<DevelopmentArtifact>{
+      DevelopmentArtifact.universal,
+      DevelopmentArtifact.androidGenSnapshot,
+      DevelopmentArtifact.androidMaven,
+      DevelopmentArtifact.androidInternalBuild,
+    }));
+  }, overrides: <Type, Generator>{
+    Cache: () => cache,
+  });
+
+  testUsingContext('precache adds artifact flags to requested artifacts on stable', () async {
+    final PrecacheCommand command = PrecacheCommand();
+    applyMocksToCommand(command);
+    await createTestCommandRunner(command).run(
+      const <String>[
+        'precache',
+        '--ios',
+        '--android_gen_snapshot',
+        '--android_maven',
+        '--android_internal_build',
+        '--web',
+        '--macos',
+        '--linux',
+        '--windows',
+        '--fuchsia',
+        '--flutter_runner',
+      ],
+    );
+    expect(artifacts, unorderedEquals(<DevelopmentArtifact>{
+      DevelopmentArtifact.universal,
+      DevelopmentArtifact.iOS,
+      DevelopmentArtifact.androidGenSnapshot,
+      DevelopmentArtifact.androidMaven,
+      DevelopmentArtifact.androidInternalBuild,
+    }));
+  }, overrides: <Type, Generator>{
+    Cache: () => cache,
+    FlutterVersion: () => flutterVersion,
+    FeatureFlags: () => TestFeatureFlags(isWebEnabled: false),
+  });
+  testUsingContext('precache downloads artifacts when --force is provided', () async {
+    when(cache.isUpToDate()).thenReturn(true);
+    final PrecacheCommand command = PrecacheCommand();
+    applyMocksToCommand(command);
+    await createTestCommandRunner(command).run(const <String>['precache', '--force']);
+    expect(artifacts, unorderedEquals(<DevelopmentArtifact>{
+      DevelopmentArtifact.universal,
+      DevelopmentArtifact.iOS,
+      DevelopmentArtifact.androidGenSnapshot,
+      DevelopmentArtifact.androidMaven,
+      DevelopmentArtifact.androidInternalBuild,
+    }));
+  }, overrides: <Type, Generator>{
+    Cache: () => cache,
+    FlutterVersion: () => flutterVersion,
   });
 }
 
diff --git a/packages/flutter_tools/test/general.shard/cache_test.dart b/packages/flutter_tools/test/general.shard/cache_test.dart
index cc6bb62..a6a20a2 100644
--- a/packages/flutter_tools/test/general.shard/cache_test.dart
+++ b/packages/flutter_tools/test/general.shard/cache_test.dart
@@ -209,7 +209,7 @@
   });
 
   test('Unstable artifacts', () {
-    expect(DevelopmentArtifact.web.unstable, true);
+    expect(DevelopmentArtifact.web.unstable, false);
     expect(DevelopmentArtifact.linux.unstable, true);
     expect(DevelopmentArtifact.macOS.unstable, true);
     expect(DevelopmentArtifact.windows.unstable, true);
diff --git a/packages/flutter_tools/test/src/testbed.dart b/packages/flutter_tools/test/src/testbed.dart
index d8db9fa..37acf09 100644
--- a/packages/flutter_tools/test/src/testbed.dart
+++ b/packages/flutter_tools/test/src/testbed.dart
@@ -712,6 +712,23 @@
 
   @override
   final bool isNewAndroidEmbeddingEnabled;
+
+  @override
+  bool isEnabled(Feature feature) {
+    switch (feature) {
+      case flutterWebFeature:
+        return isWebEnabled;
+      case flutterLinuxDesktopFeature:
+        return isLinuxEnabled;
+      case flutterMacOSDesktopFeature:
+        return isMacOSEnabled;
+      case flutterWindowsDesktopFeature:
+        return isWindowsEnabled;
+      case flutterNewAndroidEmbeddingFeature:
+        return isNewAndroidEmbeddingEnabled;
+    }
+    return false;
+  }
 }
 
 class ThrowingPub implements Pub {