Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 39 additions & 16 deletions app/src/app_menus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use itertools::Itertools;
use settings::manager::SettingsManager;
use settings::Setting as _;
use warp_core::brand;
use warp_core::channel::ChannelState;
use warp_core::context_flag::ContextFlag;
use warp_util::path::user_friendly_path;
use warpui::actions::StandardAction;
Expand Down Expand Up @@ -62,18 +63,22 @@ const MAX_RECENT_REPOS_IN_MENU: usize = 10;

/// Creates the root app menu bar
pub fn menu_bar(ctx: &mut AppContext) -> MenuBar {
MenuBar::new(vec![
let mut menus = vec![
make_new_app_menu(ctx),
make_new_file_menu(ctx),
make_new_edit_menu(ctx),
make_new_view_menu(ctx),
make_new_tab_menu(ctx),
make_new_blocks_menu(ctx),
make_new_ai_menu(ctx),
make_new_drive_menu(ctx),
make_new_window_menu(),
make_new_help_menu(),
])
];

if ChannelState::cloud_services_available() {
menus.push(make_new_drive_menu(ctx));
}

menus.extend([make_new_window_menu(), make_new_help_menu()]);
Comment thread
BunsDev marked this conversation as resolved.
MenuBar::new(menus)
}

// Creates the app dock menu
Expand Down Expand Up @@ -150,11 +155,14 @@ fn make_new_app_menu(ctx: &AppContext) -> Menu {
))
}

menu_items.extend([
MenuItem::Separator,
updateable_custom_item_without_checkmark(CustomAction::ReferAFriend, ctx),
MenuItem::Separator,
]);
menu_items.push(MenuItem::Separator);
if ChannelState::cloud_services_available() {
menu_items.push(updateable_custom_item_without_checkmark(
CustomAction::ReferAFriend,
ctx,
));
menu_items.push(MenuItem::Separator);
}

let preferences_menu_items = vec![
updateable_custom_item_without_checkmark(CustomAction::ShowSettings, ctx),
Expand Down Expand Up @@ -377,14 +385,21 @@ fn make_new_edit_menu(ctx: &AppContext) -> Menu {
}

fn make_new_view_menu(ctx: &AppContext) -> Menu {
let mut items = vec![
updateable_custom_item_without_checkmark(CustomAction::ToggleWarpDrive, ctx),
MenuItem::Separator,
let mut items = vec![];
if ChannelState::cloud_services_available() {
items.push(updateable_custom_item_without_checkmark(
CustomAction::ToggleWarpDrive,
ctx,
));
items.push(MenuItem::Separator);
}

items.extend([
updateable_custom_item_without_checkmark(CustomAction::CommandPalette, ctx),
updateable_custom_item_without_checkmark(CustomAction::NavigationPalette, ctx),
updateable_custom_item_without_checkmark(CustomAction::LaunchConfigPalette, ctx),
updateable_custom_item_without_checkmark(CustomAction::FilesPalette, ctx),
];
]);

if FeatureFlag::AgentViewConversationListView.is_enabled() {
items.push(updateable_custom_item_without_checkmark(
Expand Down Expand Up @@ -598,9 +613,17 @@ fn make_new_blocks_menu(ctx: &AppContext) -> Menu {
ctx,
));
items.push(MenuItem::Separator);
items.push(updateable_custom_item_without_checkmark(
CustomAction::CreateBlockPermalink,
ctx,
));
if ChannelState::cloud_services_available() {
items.push(non_updateable_custom_item(
CustomAction::ViewSharedBlocks,
ctx,
));
}
items.extend([
updateable_custom_item_without_checkmark(CustomAction::CreateBlockPermalink, ctx),
non_updateable_custom_item(CustomAction::ViewSharedBlocks, ctx),
updateable_custom_item_without_checkmark(CustomAction::ToggleBookmarkBlock, ctx),
updateable_custom_item_without_checkmark(CustomAction::FindWithinBlock, ctx),
MenuItem::Separator,
Expand Down
112 changes: 112 additions & 0 deletions app/src/castcodes_public_surface_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const AI_BLOCK_SOURCE: &str = include_str!("ai/blocklist/block.rs");
const AI_BLOCK_STATUS_BAR_SOURCE: &str = include_str!("ai/blocklist/block/status_bar.rs");
const AI_FACT_RULE_SOURCE: &str = include_str!("ai/facts/view/rule.rs");
const AI_TELEMETRY_BANNER_SOURCE: &str = include_str!("ai/blocklist/telemetry_banner.rs");
const APP_MENUS_SOURCE: &str = include_str!("app_menus.rs");
const AGENT_VIEW_ZERO_STATE_BLOCK_SOURCE: &str =
include_str!("ai/blocklist/agent_view/zero_state_block.rs");
const AGENT_PANEL_MOD_SOURCE: &str = include_str!("agent_panel/mod.rs");
Expand Down Expand Up @@ -54,6 +55,117 @@ const WORKFLOW_VIEW_SOURCE: &str = include_str!("workflows/workflow_view.rs");
const WORKSPACE_MOD_SOURCE: &str = include_str!("workspace/mod.rs");
const WORKSPACE_VIEW_SOURCE: &str = include_str!("workspace/view.rs");

fn function_source<'a>(source: &'a str, function: &str, next_function: &str) -> &'a str {
source
.split_once(function)
.unwrap_or_else(|| panic!("{function} should exist"))
.1
.split_once(next_function)
.unwrap_or_else(|| panic!("{next_function} should follow {function}"))
.0
}

fn matching_closing_brace(source: &str, opening_brace_offset: usize) -> Option<usize> {
if source.as_bytes().get(opening_brace_offset) != Some(&b'{') {
return None;
}

let mut depth = 0;
for (relative_offset, character) in source[opening_brace_offset..].char_indices() {
match character {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
return Some(opening_brace_offset + relative_offset);
}
}
_ => {}
}
}
None
}

fn assert_item_is_cloud_service_only(function: &str, item: &str, description: &str) {
let item_offset = function
.find(item)
.unwrap_or_else(|| panic!("{description} should exist"));
let cloud_gate = "if ChannelState::cloud_services_available()";
let gate_offset = function[..item_offset]
.rfind(cloud_gate)
.unwrap_or_else(|| panic!("{description} should have a cloud-service gate"));
let gate_open_offset = function[gate_offset..item_offset]
.find('{')
.map(|offset| gate_offset + offset)
.unwrap_or_else(|| panic!("{description} cloud-service gate should open"));
let gate_end_offset = matching_closing_brace(function, gate_open_offset)
.unwrap_or_else(|| panic!("{description} cloud-service gate should close"));

assert!(
item_offset < gate_end_offset,
"{description} should be hidden when hosted cloud services are unavailable"
);
}

#[test]
fn cloud_service_gate_matching_supports_nested_braces() {
let function = r#"
if ChannelState::cloud_services_available() {
if nested_condition {
nested_action();
}
CustomAction::ReferAFriend;
}
"#;
assert_item_is_cloud_service_only(
function,
"CustomAction::ReferAFriend",
"nested referral action",
);
}

#[test]
fn referral_app_menu_item_is_cloud_service_only() {
let app_menu = function_source(
APP_MENUS_SOURCE,
"fn make_new_app_menu",
"fn make_new_file_menu",
);
assert_item_is_cloud_service_only(
app_menu,
"CustomAction::ReferAFriend",
"referral app menu action",
);
}

#[test]
fn oss_app_menus_do_not_build_cloud_only_items() {
let menu_bar = function_source(APP_MENUS_SOURCE, "pub fn menu_bar", "pub fn dock_menu");
assert_item_is_cloud_service_only(menu_bar, "make_new_drive_menu(ctx)", "Drive app menu");

let view_menu = function_source(
APP_MENUS_SOURCE,
"fn make_new_view_menu",
"fn make_new_tab_menu",
);
assert_item_is_cloud_service_only(
view_menu,
"CustomAction::ToggleWarpDrive",
"Cast Drive view menu action",
);

let blocks_menu = function_source(
APP_MENUS_SOURCE,
"fn make_new_blocks_menu",
"fn make_new_drive_menu",
);
assert_item_is_cloud_service_only(
blocks_menu,
"CustomAction::ViewSharedBlocks",
"shared blocks menu action",
);
}

#[test]
fn public_app_surfaces_use_castcodes_links_and_labels() {
assert!(LOGIN_SLIDE_SOURCE.contains("PRIVACY_POLICY_URL"));
Expand Down
28 changes: 20 additions & 8 deletions crates/integration/src/bin/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,17 @@ pub struct Args {
}

pub fn main() -> Result<()> {
let args = Args::parse();
let tests = register_tests();
let channel = args
.integration_test_name
.as_deref()
.and_then(|test_name| tests.get(test_name))
.map(|(channel, _)| *channel)
.unwrap_or(Channel::Integration);

ChannelState::set(ChannelState::new(
Channel::Integration,
channel,
ChannelConfig {
app_id: AppId::new(
"dev",
Expand Down Expand Up @@ -56,8 +65,6 @@ pub fn main() -> Result<()> {
},
));

let args = Args::parse();

if let Some(command) = &args.command {
match command {
#[cfg(unix)]
Expand All @@ -75,14 +82,13 @@ pub fn main() -> Result<()> {
}
}

let tests = register_tests();
let test_name = args
.integration_test_name
.as_deref()
.expect("Integration test name is required");

println!("Running integration test: {test_name}");
let Some(builder) = tests.get(test_name).map(|func| func()) else {
let Some(builder) = tests.get(test_name).map(|(_, builder_fn)| builder_fn()) else {
panic!("test not found for args: {:#?}", env::args());
};
#[cfg_attr(not(unix), allow(unused_variables))]
Expand All @@ -109,21 +115,27 @@ pub fn main() -> Result<()> {

/// Type of a function that produces an integration test builder.
type BoxedBuilderFn = Box<dyn Fn() -> Builder>;
type RegisteredTest = (Channel, BoxedBuilderFn);

fn register_tests() -> HashMap<&'static str, BoxedBuilderFn> {
let mut tests: HashMap<&str, BoxedBuilderFn> = HashMap::new();
fn register_tests() -> HashMap<&'static str, RegisteredTest> {
let mut tests: HashMap<&str, RegisteredTest> = HashMap::new();

// A tiny macro to simplify the act of registering a test. This avoids
// any inconsistencies between the test function name and the key in the
// map, and makes it easier to change how we register the tests (if we
// decide to do so in the future).
macro_rules! register_test {
($name:ident) => {
tests.insert(stringify!($name), Box::new(|| $name()));
register_test!($name, Channel::Integration);
};
($name:ident, $channel:expr) => {
tests.insert(stringify!($name), ($channel, Box::new(|| $name())));
};
}

// Add new tests here
#[cfg(target_os = "macos")]
register_test!(test_oss_app_menu_startup, Channel::Oss);
register_test!(test_single_command);
register_test!(test_add_and_close_session);
register_test!(test_add_many_sessions);
Expand Down
4 changes: 4 additions & 0 deletions crates/integration/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

mod agent_mode;
mod agent_panel;
#[cfg(target_os = "macos")]
mod app_startup;
mod block_filtering;
mod bootstrapping;
mod code_review;
Expand Down Expand Up @@ -36,6 +38,8 @@ mod workspace;

pub use agent_mode::*;
pub use agent_panel::*;
#[cfg(target_os = "macos")]
pub use app_startup::*;
pub use block_filtering::*;
pub use bootstrapping::*;
pub use code_review::*;
Expand Down
20 changes: 20 additions & 0 deletions crates/integration/src/test/app_startup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use crate::Builder;
use warpui::integration::{AssertionOutcome, TestStep};

pub fn test_oss_app_menu_startup() -> Builder {
Builder::new().with_step(TestStep::new("Assert OSS app startup").add_named_assertion(
"initial root view exists",
|app, window_id| {
if app
.root_view::<warp::root_view::RootView>(window_id)
.is_some()
{
AssertionOutcome::Success
} else {
AssertionOutcome::failure(format!(
"root view should exist for window_id={window_id}"
))
}
},
))
}
2 changes: 2 additions & 0 deletions crates/integration/tests/integration/ui_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use super::integration_tests;

integration_tests! {
#[cfg(target_os = "macos")]
test_oss_app_menu_startup,
test_add_many_sessions,
test_daemon_conversation_composer_names_daemon_fix,
test_ctrl_tab_session_switching,
Expand Down
Loading