From 4669ba92b0e5ac09d9aa3443661f39bd61cf2c5a Mon Sep 17 00:00:00 2001 From: dal Date: Mon, 12 May 2025 16:33:53 -0600 Subject: [PATCH 1/4] quick debug on snowflake --- .../src/data_source_query_routes/snowflake_query.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/libs/query_engine/src/data_source_query_routes/snowflake_query.rs b/api/libs/query_engine/src/data_source_query_routes/snowflake_query.rs index cd9a14837..825249f02 100644 --- a/api/libs/query_engine/src/data_source_query_routes/snowflake_query.rs +++ b/api/libs/query_engine/src/data_source_query_routes/snowflake_query.rs @@ -1645,7 +1645,7 @@ fn prepare_query(query: &str) -> String { } fn process_record_batch(batch: &RecordBatch) -> Vec> { - // println!("Processing record batch with {:?} rows", batch); + println!("Processing record batch with {:?} rows", batch); let mut rows = Vec::with_capacity(batch.num_rows()); let schema = batch.schema(); From 84d25a1456375789cfa26314e03e1f964b24cf9c Mon Sep 17 00:00:00 2001 From: dal Date: Mon, 12 May 2025 17:05:41 -0600 Subject: [PATCH 2/4] snowlfake scale 9 --- .../snowflake_query.rs | 360 +++++++++++++++--- 1 file changed, 306 insertions(+), 54 deletions(-) diff --git a/api/libs/query_engine/src/data_source_query_routes/snowflake_query.rs b/api/libs/query_engine/src/data_source_query_routes/snowflake_query.rs index 825249f02..03137c93c 100644 --- a/api/libs/query_engine/src/data_source_query_routes/snowflake_query.rs +++ b/api/libs/query_engine/src/data_source_query_routes/snowflake_query.rs @@ -84,6 +84,7 @@ fn handle_snowflake_timestamp(value: &Value) -> Option { fn handle_snowflake_timestamp_struct( struct_array: &arrow::array::StructArray, row_idx: usize, + scale: Option, // Add scale parameter ) -> Option> { if struct_array.is_null(row_idx) { return None; @@ -108,36 +109,54 @@ fn handle_snowflake_timestamp_struct( fraction.value(row_idx) }; - // Important: Check if epoch might be in milliseconds instead of seconds - // If the epoch value is larger than typical Unix timestamps (e.g., > 50 years worth of seconds) - // it's likely in milliseconds or microseconds + // Determine epoch/nanos based on epoch_value magnitude AND scale let (adjusted_epoch, adjusted_nanos) = if epoch_value > 5_000_000_000 { - // Likely milliseconds or microseconds - determine which + // Epoch likely in ms or us if epoch_value > 5_000_000_000_000 { // Microseconds ( epoch_value / 1_000_000, - (epoch_value % 1_000_000 * 1000) as u32, + (epoch_value % 1_000_000 * 1000).try_into().unwrap_or(0), // Convert to u32 safely ) } else { // Milliseconds - (epoch_value / 1000, (epoch_value % 1000 * 1_000_000) as u32) + ( + epoch_value / 1000, + (epoch_value % 1000 * 1_000_000).try_into().unwrap_or(0), // Convert to u32 safely + ) } } else { - // Seconds - use fraction for nanoseconds - // For scale 3 (milliseconds), multiply by 10^6 to get nanoseconds - (epoch_value, (fraction_value as u32) * 1_000_000) + // Epoch is likely in seconds, use scale to interpret fraction + let calculated_nanos = match scale { + Some(3) => (fraction_value as i64 * 1_000_000).try_into().unwrap_or(0), // Milliseconds to nanos + Some(6) => (fraction_value as i64 * 1000).try_into().unwrap_or(0), // Microseconds to nanos + Some(9) => fraction_value.try_into().unwrap_or(0), // Fraction IS nanos + _ => { // Default or unknown scale, assume fraction is nanos if < 1B, else 0 + if fraction_value >= 0 && fraction_value < 1_000_000_000 { + fraction_value as u32 + } else { + tracing::warn!( + "Unhandled scale ({:?}) or invalid fraction ({}) for seconds epoch, defaulting nanos to 0", + scale, + fraction_value + ); + 0 + } + } + }; + (epoch_value, calculated_nanos) }; match parse_snowflake_timestamp(adjusted_epoch, adjusted_nanos) { Ok(dt) => Some(dt), Err(e) => { - tracing::error!("Failed to parse timestamp: {}", e); + tracing::error!("Failed to parse timestamp: {}. adjusted_epoch={}, adjusted_nanos={}. Original epoch={}, fraction={}, scale={:?}", + e, adjusted_epoch, adjusted_nanos, epoch_value, fraction_value, scale); None } } } - _ => None, + _ => None, // Epoch or fraction array missing or epoch is null } } @@ -1253,9 +1272,13 @@ fn handle_struct_array( // Check if this is a Snowflake timestamp struct let fields = match field.data_type() { ArrowDataType::Struct(fields) => fields, - _ => return DataType::Null, + _ => return DataType::Null, // Should not happen if called with a struct field }; + // Try to get scale from metadata + let scale_meta_str = field.metadata().get("scale"); + let scale: Option = scale_meta_str.and_then(|s| s.parse::().ok()); + if fields.len() == 2 && fields.iter().any(|f| f.name() == "epoch") && fields.iter().any(|f| f.name() == "fraction") @@ -1264,7 +1287,7 @@ fn handle_struct_array( .get("logicalType") .map_or(false, |v| v.contains("TIMESTAMP")) { - if let Some(dt) = handle_snowflake_timestamp_struct(array, row_idx) { + if let Some(dt) = handle_snowflake_timestamp_struct(array, row_idx, scale) { // Pass scale here if field .metadata() .get("logicalType") @@ -1283,21 +1306,23 @@ fn handle_struct_array( DataType::Null } else { let mut map = JsonMap::new(); - for (field, col) in fields.iter().zip(array.columns().iter()) { - let field_name = field.name(); + for (struct_field_def, col) in fields.iter().zip(array.columns().iter()) { + let field_name = struct_field_def.name(); // Use name from struct_field_def let value = if col.is_null(row_idx) { Value::Null - } else if let Some(array) = col.as_any().downcast_ref::() { - Value::Number(array.value(row_idx).into()) - } else if let Some(array) = col.as_any().downcast_ref::() { - Value::Number(array.value(row_idx).into()) - } else if let Some(array) = col.as_any().downcast_ref::() { - serde_json::Number::from_f64(array.value(row_idx)) + } else if let Some(arr) = col.as_any().downcast_ref::() { + Value::Number(arr.value(row_idx).into()) + } else if let Some(arr) = col.as_any().downcast_ref::() { + Value::Number(arr.value(row_idx).into()) + } else if let Some(arr) = col.as_any().downcast_ref::() { + serde_json::Number::from_f64(arr.value(row_idx)) .map(Value::Number) .unwrap_or(Value::Null) - } else if let Some(array) = col.as_any().downcast_ref::() { - Value::String(array.value(row_idx).to_string()) + } else if let Some(arr) = col.as_any().downcast_ref::() { + Value::String(arr.value(row_idx).to_string()) } else { + // Attempt to handle nested structs recursively or other types if needed + // For now, defaulting to Null for unhandled types within generic structs Value::Null }; map.insert(field_name.to_string(), value); @@ -1384,81 +1409,106 @@ fn convert_array_to_datatype( } ArrowDataType::Int64 => { let field_name = field.name(); // Get field name for logging - // println!("Debug: Processing Int64 field: {}", field_name); + // **NEW LOGGING START** + tracing::debug!( + "Processing Int64 field: '{}', row_idx: {}, Data Type: {:?}, Metadata: {:?}", + field_name, + row_idx, + column.data_type(), + field.metadata() + ); + // **NEW LOGGING END** // Check if this is actually a timestamp in disguise let logical_type = field.metadata().get("logicalType"); let scale_str = field.metadata().get("scale"); // Get scale_str here as well - // println!("Debug [{}]: logicalType={:?}, scale={:?}", field_name, logical_type, scale_str); if logical_type.map_or(false, |t| t.contains("TIMESTAMP")) { - // println!("Debug [{}]: Detected as timestamp.", field_name); + // **MODIFIED LOGGING** + tracing::debug!("[{}]: Detected as timestamp. logicalType={:?}, scale={:?}", field_name, logical_type, scale_str); // If it has a timestamp logical type, determine the time unit based on scale - let unit = match scale_str.map(|s| s.parse::().unwrap_or(3)) { - // Default parse to 3 (ms) + let unit = match scale_str.map(|s| s.parse::().unwrap_or(3)) { // Default parse to 3 (ms) Some(0) => TimeUnit::Second, Some(6) => TimeUnit::Microsecond, Some(9) => TimeUnit::Nanosecond, Some(3) | None | Some(_) => TimeUnit::Millisecond, // Default to millisecond }; - // println!("Debug [{}]: Determined unit: {:?}", field_name, unit); + // **MODIFIED LOGGING** + tracing::debug!("[{}]: Determined unit: {:?}", field_name, unit); // Check if there's timezone info let has_tz = logical_type.map_or(false, |t| t.contains("_TZ")); - // println!("Debug [{}]: has_tz: {}", field_name, has_tz); - let _tz: Option> = if has_tz { - Some(Arc::new(String::from("UTC"))) - } else { - None - }; + // **MODIFIED LOGGING** + tracing::debug!("[{}]: has_tz: {}", field_name, has_tz); + let _tz: Option> = if has_tz { Some(Arc::new(String::from("UTC"))) } else { None }; // Process as timestamp if let Some(array) = column.as_any().downcast_ref::() { if array.is_null(row_idx) { - // println!("Debug [{}]: Value is null at row_idx {}.", field_name, row_idx); + tracing::debug!("[{}]: Value is null at row_idx {}.", field_name, row_idx); return DataType::Null; } let value = array.value(row_idx); - // println!("Debug [{}]: Raw value at row_idx {}: {}", field_name, row_idx, value); + // **MODIFIED LOGGING** + tracing::debug!("[{}]: Raw value at row_idx {}: {}", field_name, row_idx, value); + + // **NEW LOGGING START** let (secs, subsec_nanos) = match unit { TimeUnit::Second => (value, 0), TimeUnit::Millisecond => (value / 1000, (value % 1000) * 1_000_000), TimeUnit::Microsecond => (value / 1_000_000, (value % 1_000_000) * 1000), TimeUnit::Nanosecond => (value / 1_000_000_000, value % 1_000_000_000), }; - // println!("Debug [{}]: Calculated secs={}, nanos={}", field_name, secs, subsec_nanos); + tracing::debug!("[{}]: Calculated secs={}, nanos={}", field_name, secs, subsec_nanos); + // **NEW LOGGING END** + // **NEW LOGGING START** + tracing::debug!( + "[{}]: Calling Utc.timestamp_opt({}, {})", + field_name, + secs, + subsec_nanos + ); + // **NEW LOGGING END** match Utc.timestamp_opt(secs, subsec_nanos as u32) { LocalResult::Single(dt) => { - // println!("Debug [{}]: Successfully created DateTime: {}", field_name, dt); + tracing::debug!("[{}]: Successfully created DateTime: {}", field_name, dt); if has_tz { - // println!("Debug [{}]: Returning Timestamptz.", field_name); + tracing::debug!("[{}]: Returning Timestamptz.", field_name); DataType::Timestamptz(Some(dt)) } else { - // println!("Debug [{}]: Returning Timestamp.", field_name); + tracing::debug!("[{}]: Returning Timestamp.", field_name); DataType::Timestamp(Some(dt.naive_utc())) } } LocalResult::None | LocalResult::Ambiguous(_, _) => { // Handle None and Ambiguous explicitly - tracing::error!("Failed to create DateTime (None or Ambiguous) from timestamp: secs={}, nanos={}", secs, subsec_nanos); - // println!("Debug [{}]: Failed to create DateTime (None or Ambiguous) from timestamp: secs={}, nanos={}", field_name, secs, subsec_nanos); + // **NEW LOGGING START** + tracing::error!( + "[{}]: Utc.timestamp_opt failed (returned None or Ambiguous) for secs={}, nanos={}. Raw value was {}. Returning Null.", + field_name, secs, subsec_nanos, value + ); + // **NEW LOGGING END** DataType::Null } } } else { - // println!("Debug [{}]: Failed to downcast to Int64Array.", field_name); + // **MODIFIED LOGGING** + tracing::warn!("[{}]: Failed to downcast Int64 (identified as timestamp) to Int64Array.", field_name); DataType::Null } } else { // Not a timestamp, so delegate to handle_int64_array which can handle scaling or default to Int8 - if let Some(array) = column.as_any().downcast_ref::() { + // **MODIFIED LOGGING** + tracing::debug!("[{}]: Not identified as timestamp based on metadata. Delegating to handle_int64_array.", field_name); + if let Some(array) = column.as_any().downcast_ref::() { handle_int64_array(array, row_idx, scale_str.map(|s| s.as_str()), field) - } else { - // println!("Debug [{}]: Failed to downcast Int64 for non-timestamp to Int64Array.", field_name); + } else { + // **MODIFIED LOGGING** + tracing::warn!("[{}]: Failed to downcast Int64 (non-timestamp) to Int64Array.", field_name); DataType::Null - } + } } } ArrowDataType::UInt8 => { @@ -1992,7 +2042,7 @@ mod tests { ), ]); - let dt = handle_snowflake_timestamp_struct(&struct_array, 0); + let dt = handle_snowflake_timestamp_struct(&struct_array, 0, field.metadata().get("scale").and_then(|s| s.parse::().ok())); println!( "handle_snowflake_timestamp_struct (seconds epoch, millis fraction): {:?}", dt @@ -2024,7 +2074,7 @@ mod tests { ), ]); - let dt = handle_snowflake_timestamp_struct(&struct_array, 0); + let dt = handle_snowflake_timestamp_struct(&struct_array, 0, field.metadata().get("scale").and_then(|s| s.parse::().ok())); println!( "handle_snowflake_timestamp_struct (millis epoch, zero fraction): {:?}", dt @@ -2067,7 +2117,7 @@ mod tests { ), ]); - let dt = handle_snowflake_timestamp_struct(&struct_array, 0); + let dt = handle_snowflake_timestamp_struct(&struct_array, 0, field.metadata().get("scale").and_then(|s| s.parse::().ok())); println!( "handle_snowflake_timestamp_struct (microsecs epoch): {:?}", dt @@ -2398,7 +2448,7 @@ mod tests { ]); // Call the function directly - let result = handle_snowflake_timestamp_struct(&struct_array, 0); + let result = handle_snowflake_timestamp_struct(&struct_array, 0, None); // Print and verify result if let Some(dt) = result { @@ -2476,8 +2526,8 @@ mod tests { ), ]); - // Test direct function - let result = handle_snowflake_timestamp_struct(&struct_array, 0); + // Test direct function - Pass None for scale + let result = handle_snowflake_timestamp_struct(&struct_array, 0, None); assert!(result.is_none(), "Expected None for null epoch"); println!("✓ Null epoch correctly returns None"); @@ -3008,5 +3058,207 @@ mod tests { println!("✓ Verified Int64 FIXED with Scale processing"); } + + /// Tests processing a RecordBatch with Struct-based timestamps (scale 9) + /// based on real-world log output. + #[test] + fn test_struct_timestamp_scale9_processing() { + println!("\n=== Testing Struct TIMESTAMP_NTZ(9) processing ==="); + + // --- Sample Data (Anonymized, based on provided log) --- + // Selected a few representative rows + let epoch_data = vec![ + Some(1736442980i64), // Row 0 + Some(1736443293i64), // Row 4 + None, // Simulate a potential null row + Some(1737408291i64), // Last row + ]; + let fraction_data = vec![ + Some(969000000i32), // Row 0 (0.969 seconds) + Some(555000000i32), // Row 4 (0.555 seconds) + Some(123456789i32), // Null epoch row, fraction irrelevant but needs value + Some(504000000i32), // Last row (0.504 seconds) + ]; + let product_name_data = vec![ + Some("Product A"), + Some("Product B"), + Some("Product C"), + Some("Product D"), + ]; + let sku_data = vec![Some("SKU-A"), Some("SKU-B"), Some("SKU-C"), Some("SKU-D")]; + let order_number_data = vec![ + Some("ORD-111"), + Some("ORD-222"), + Some("ORD-333"), + Some("ORD-444"), + ]; + + // --- Array Creation --- + let epoch_array = Int64Array::from(epoch_data.clone()); + let fraction_array = Int32Array::from(fraction_data.clone()); + let product_name_array = StringArray::from(product_name_data); + let sku_array = StringArray::from(sku_data); + let order_number_array = StringArray::from(order_number_data); + + // --- Struct Array for Timestamp --- + let mut timestamp_metadata = std::collections::HashMap::new(); + timestamp_metadata.insert("logicalType".to_string(), "TIMESTAMP_NTZ".to_string()); + timestamp_metadata.insert("scale".to_string(), "9".to_string()); // Crucial: scale is 9 + + let struct_fields = Fields::from(vec![ + Field::new( + "epoch", + ArrowDataType::Int64, + true, // epoch is nullable + ) + .with_metadata(timestamp_metadata.clone()), // Metadata might be on inner fields too + Field::new( + "fraction", + ArrowDataType::Int32, + true, // fraction is nullable + ) + .with_metadata(timestamp_metadata.clone()), + ]); + + // Create the StructArray + let struct_array = StructArray::new( + struct_fields.clone(), + vec![ + Arc::new(epoch_array) as ArrayRef, + Arc::new(fraction_array) as ArrayRef, + ], + // Set the validity based on the epoch_data's nulls + Some(arrow::buffer::NullBuffer::from( + epoch_data.iter().map(|x| x.is_some()).collect::>(), + )), + ); + + // --- Field Creation --- + let field_returned_processed_date = Field::new( + "RETURNED_PROCESSED_DATE", + ArrowDataType::Struct(struct_fields), + true, // The struct itself is nullable + ) + .with_metadata(timestamp_metadata.clone()); // Metadata on the struct field itself + + let field_product_name = Field::new("PRODUCT_NAME", ArrowDataType::Utf8, true); + let field_sku = Field::new("SKU", ArrowDataType::Utf8, true); + let field_order_number = Field::new("ORDER_NUMBER", ArrowDataType::Utf8, true); + + // --- Schema Creation --- + let schema = Arc::new(Schema::new(vec![ + field_returned_processed_date, + field_product_name, + field_sku, + field_order_number, + ])); + + // --- RecordBatch Creation --- + let batch = RecordBatch::try_new( + schema.clone(), + vec![ + Arc::new(struct_array) as ArrayRef, + Arc::new(product_name_array) as ArrayRef, + Arc::new(sku_array) as ArrayRef, + Arc::new(order_number_array) as ArrayRef, + ], + ) + .unwrap(); + + println!("Simulated Input RecordBatch schema: {:?}", batch.schema()); + + // --- Process Batch --- + let processed_rows = process_record_batch(&batch); + + println!("Processed Rows (Struct Timestamp Scale 9): {:?}", processed_rows); + + // --- Assertions --- + assert_eq!(processed_rows.len(), 4, "Expected 4 rows processed"); + + // Helper to create expected NaiveDateTime from secs/nanos + let dt_from_parts = |secs: i64, nanos: u32| Utc.timestamp_opt(secs, nanos).unwrap().naive_utc(); + + // Row 0 Assertions (epoch=1736442980, fraction=969000000) -> nanos=969000000 + let expected_dt_0 = dt_from_parts(1736442980, 969000000); + assert_eq!( + processed_rows[0]["returned_processed_date"], // field name is lowercased + DataType::Timestamp(Some(expected_dt_0)), + "Row 0 timestamp mismatch" + ); + assert_eq!( + processed_rows[0]["product_name"], + DataType::Text(Some("Product A".to_string())) + ); + assert_eq!( + processed_rows[0]["sku"], + DataType::Text(Some("SKU-A".to_string())) + ); + assert_eq!( + processed_rows[0]["order_number"], + DataType::Text(Some("ORD-111".to_string())) + ); + + // Row 1 Assertions (epoch=1736443293, fraction=555000000) -> nanos=555000000 + let expected_dt_1 = dt_from_parts(1736443293, 555000000); + assert_eq!( + processed_rows[1]["returned_processed_date"], + DataType::Timestamp(Some(expected_dt_1)), + "Row 1 timestamp mismatch" + ); + assert_eq!( + processed_rows[1]["product_name"], + DataType::Text(Some("Product B".to_string())) + ); + assert_eq!( + processed_rows[1]["sku"], + DataType::Text(Some("SKU-B".to_string())) + ); + assert_eq!( + processed_rows[1]["order_number"], + DataType::Text(Some("ORD-222".to_string())) + ); + + // Row 2 Assertions (Null epoch) + assert_eq!( + processed_rows[2]["returned_processed_date"], + DataType::Null, + "Row 2 timestamp should be Null" + ); + assert_eq!( + processed_rows[2]["product_name"], + DataType::Text(Some("Product C".to_string())) + ); + assert_eq!( + processed_rows[2]["sku"], + DataType::Text(Some("SKU-C".to_string())) + ); + assert_eq!( + processed_rows[2]["order_number"], + DataType::Text(Some("ORD-333".to_string())) + ); + + + // Row 3 Assertions (epoch=1737408291, fraction=504000000) -> nanos=504000000 + let expected_dt_3 = dt_from_parts(1737408291, 504000000); + assert_eq!( + processed_rows[3]["returned_processed_date"], + DataType::Timestamp(Some(expected_dt_3)), + "Row 3 timestamp mismatch" + ); + assert_eq!( + processed_rows[3]["product_name"], + DataType::Text(Some("Product D".to_string())) + ); + assert_eq!( + processed_rows[3]["sku"], + DataType::Text(Some("SKU-D".to_string())) + ); + assert_eq!( + processed_rows[3]["order_number"], + DataType::Text(Some("ORD-444".to_string())) + ); + + println!("✓ Verified Struct TIMESTAMP_NTZ(9) processing"); + } } From 2a6d3b4daeb98f36b43d1f485684dbb115bde276 Mon Sep 17 00:00:00 2001 From: dal Date: Mon, 12 May 2025 17:18:44 -0600 Subject: [PATCH 3/4] few more snowflake tests --- .../snowflake_query.rs | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) diff --git a/api/libs/query_engine/src/data_source_query_routes/snowflake_query.rs b/api/libs/query_engine/src/data_source_query_routes/snowflake_query.rs index 03137c93c..59adb3873 100644 --- a/api/libs/query_engine/src/data_source_query_routes/snowflake_query.rs +++ b/api/libs/query_engine/src/data_source_query_routes/snowflake_query.rs @@ -3260,5 +3260,264 @@ mod tests { println!("✓ Verified Struct TIMESTAMP_NTZ(9) processing"); } + + /// Tests processing Int64 arrays with TIMESTAMP_NTZ metadata and various scales. + #[test] + fn test_int64_timestamp_ntz_various_scales() { + println!("\n=== Testing Int64 TIMESTAMP_NTZ with various scales ==="); + + // --- Sample Data --- + // Use a consistent base time for easier verification + let base_secs = 1700000000i64; // 2023-11-14 22:13:20 UTC + // Define expected nanos carefully + let expected_nanos = 123456789u32; + + // Calculate Int64 values based on scale + let data_scale0 = vec![Some(base_secs)]; // Value is seconds + let data_scale6 = vec![Some(base_secs * 1_000_000 + (expected_nanos / 1000) as i64)]; // Value is microseconds + let data_scale9 = vec![Some(base_secs * 1_000_000_000 + expected_nanos as i64)]; // Value is nanoseconds + let data_null = vec![None::]; + + // --- Array Creation --- + let array_scale0 = Int64Array::from(data_scale0); + let array_scale6 = Int64Array::from(data_scale6); + let array_scale9 = Int64Array::from(data_scale9); + let array_null = Int64Array::from(data_null); + + // --- Metadata and Field Creation --- + let mut meta_ntz_scale0 = std::collections::HashMap::new(); + meta_ntz_scale0.insert("logicalType".to_string(), "TIMESTAMP_NTZ".to_string()); + meta_ntz_scale0.insert("scale".to_string(), "0".to_string()); + let field_scale0 = Field::new("ts_scale0", ArrowDataType::Int64, true).with_metadata(meta_ntz_scale0); + + let mut meta_ntz_scale6 = std::collections::HashMap::new(); + meta_ntz_scale6.insert("logicalType".to_string(), "TIMESTAMP_NTZ".to_string()); + meta_ntz_scale6.insert("scale".to_string(), "6".to_string()); + let field_scale6 = Field::new("ts_scale6", ArrowDataType::Int64, true).with_metadata(meta_ntz_scale6); + + let mut meta_ntz_scale9 = std::collections::HashMap::new(); + meta_ntz_scale9.insert("logicalType".to_string(), "TIMESTAMP_NTZ".to_string()); + meta_ntz_scale9.insert("scale".to_string(), "9".to_string()); + let field_scale9 = Field::new("ts_scale9", ArrowDataType::Int64, true).with_metadata(meta_ntz_scale9.clone()); // Clone for null field + + // Field for null test (metadata shouldn't matter but use one) + let field_null = Field::new("ts_null", ArrowDataType::Int64, true).with_metadata(meta_ntz_scale9); + + // --- Schema and RecordBatch --- + let schema = Arc::new(Schema::new(vec![field_scale0, field_scale6, field_scale9, field_null])); + let batch = RecordBatch::try_new( + schema, + vec![ + Arc::new(array_scale0) as ArrayRef, + Arc::new(array_scale6) as ArrayRef, + Arc::new(array_scale9) as ArrayRef, + Arc::new(array_null) as ArrayRef, + ], + ).unwrap(); + + // --- Process Batch --- + let processed_rows = process_record_batch(&batch); + + // --- Assertions --- + assert_eq!(processed_rows.len(), 1, "Expected 1 row"); + let row = &processed_rows[0]; + + // Calculate the final expected NaiveDateTime based ONLY on base_secs and expected_nanos + // Note: For scale 0, the input data doesn't contain nano precision, so the expected result *should* reflect that loss. + let expected_dt_s0 = Utc.timestamp_opt(base_secs, 0).unwrap().naive_utc(); // Scale 0 loses nanos + + // For scale 6, we only have microsecond precision (the last 3 digits of nanos are truncated) + let microsecond_nanos = (expected_nanos / 1000) * 1000; // Truncate to microsecond precision + let expected_dt_s6 = Utc.timestamp_opt(base_secs, microsecond_nanos).unwrap().naive_utc(); + + // For scale 9, we have full nanosecond precision + let expected_dt_s9 = Utc.timestamp_opt(base_secs, expected_nanos).unwrap().naive_utc(); + + // Scale 0 (Seconds) - Loses nanosecond precision from original `expected_nanos` + assert_eq!(row["ts_scale0"], DataType::Timestamp(Some(expected_dt_s0))); + // Scale 6 (Microseconds) - Should only have microsecond precision (truncated) + assert_eq!(row["ts_scale6"], DataType::Timestamp(Some(expected_dt_s6))); + // Scale 9 (Nanoseconds) - Should retain full nanosecond precision + assert_eq!(row["ts_scale9"], DataType::Timestamp(Some(expected_dt_s9))); + // Null value + assert_eq!(row["ts_null"], DataType::Null); + + println!("✓ Verified Int64 TIMESTAMP_NTZ with various scales"); + } + + /// Tests processing Int64 arrays with TIMESTAMP_TZ metadata and scale 3. + #[test] + fn test_int64_timestamp_tz_scale3() { + println!("\n=== Testing Int64 TIMESTAMP_TZ with scale 3 ==="); + + // --- Sample Data --- + let base_secs = 1700000000i64; + let base_millis = 123i64; + let data_millis = vec![Some(base_secs * 1000 + base_millis)]; // Milliseconds since epoch + let data_null = vec![None::]; + + // --- Array Creation --- + let array_data = Int64Array::from(data_millis); + let array_null = Int64Array::from(data_null); + + // --- Metadata and Field Creation --- + let mut meta_tz_scale3 = std::collections::HashMap::new(); + meta_tz_scale3.insert("logicalType".to_string(), "TIMESTAMP_TZ".to_string()); + meta_tz_scale3.insert("scale".to_string(), "3".to_string()); + let field_data = Field::new("ts_tz_scale3", ArrowDataType::Int64, true).with_metadata(meta_tz_scale3.clone()); + let field_null = Field::new("ts_null", ArrowDataType::Int64, true).with_metadata(meta_tz_scale3); + + // --- Schema and RecordBatch --- + let schema = Arc::new(Schema::new(vec![field_data, field_null])); + let batch = RecordBatch::try_new( + schema, + vec![ + Arc::new(array_data) as ArrayRef, + Arc::new(array_null) as ArrayRef, + ], + ).unwrap(); + + // --- Process Batch --- + let processed_rows = process_record_batch(&batch); + + // --- Assertions --- + assert_eq!(processed_rows.len(), 1, "Expected 1 row"); + let row = &processed_rows[0]; + + let expected_dt_utc = Utc.timestamp_millis_opt(base_secs * 1000 + base_millis).unwrap(); + + // TZ Scale 3 (Milliseconds) + assert_eq!(row["ts_tz_scale3"], DataType::Timestamptz(Some(expected_dt_utc))); + // Null value + assert_eq!(row["ts_null"], DataType::Null); + + println!("✓ Verified Int64 TIMESTAMP_TZ with scale 3"); + } + + /// Tests processing Struct timestamps with various scales and TZ/NTZ metadata. + #[test] + fn test_struct_timestamp_various_scales_and_tz() { + println!("\n=== Testing Struct Timestamps with various scales and TZ/NTZ ==="); + + // Helper function to create test structs with different scales + fn create_test_case(epoch_value: i64, fraction_value: i32, scale: i32, is_tz: bool) -> (StructArray, Field) { + let epoch_array = Int64Array::from(vec![epoch_value]); + let fraction_array = Int32Array::from(vec![fraction_value]); + + let struct_fields = Fields::from(vec![ + Arc::new(Field::new("epoch", ArrowDataType::Int64, false)), + Arc::new(Field::new("fraction", ArrowDataType::Int32, false)), + ]); + + let struct_array = StructArray::from(vec![ + ( + Arc::new(Field::new("epoch", ArrowDataType::Int64, false)), + Arc::new(epoch_array) as arrow::array::ArrayRef, + ), + ( + Arc::new(Field::new("fraction", ArrowDataType::Int32, false)), + Arc::new(fraction_array) as arrow::array::ArrayRef, + ), + ]); + + // Create field with metadata indicating this is a timestamp + let mut struct_metadata = std::collections::HashMap::new(); + struct_metadata.insert("scale".to_string(), scale.to_string()); + struct_metadata.insert( + "logicalType".to_string(), + if is_tz { + "TIMESTAMP_TZ".to_string() + } else { + "TIMESTAMP_NTZ".to_string() + }, + ); + + let struct_field = Field::new( + "TIMESTAMP_STRUCT", + ArrowDataType::Struct(struct_fields), + false, + ).with_metadata(struct_metadata); + + (struct_array, struct_field) + } + + // Base timestamp values for testing + let base_secs = 1700000000i64; // 2023-11-14 22:13:20 UTC + + // Test cases for different scales + // (epoch, fraction, scale, is_tz, expected_subsec_nanos) + let test_cases = vec![ + // Scale 3 (milliseconds) + (base_secs, 123, 3, false, 123_000_000), // 123 milliseconds → 123,000,000 nanos + (base_secs, 123, 3, true, 123_000_000), // Same with TZ + + // Scale 6 (microseconds) + (base_secs, 123456, 6, false, 123_456_000), // 123,456 microseconds → 123,456,000 nanos + (base_secs, 123456, 6, true, 123_456_000), // Same with TZ + + // Scale 9 (nanoseconds) - most important case to test + (base_secs, 123456789, 9, false, 123_456_789), // 123,456,789 nanoseconds directly + (base_secs, 123456789, 9, true, 123_456_789), // Same with TZ + + // Edge cases + (base_secs, 0, 9, false, 0), // Zero fraction + (base_secs, 999_999_999, 9, false, 999_999_999), // Max nanoseconds + ]; + + // Process each test case + for (idx, (epoch, fraction, scale, is_tz, expected_nanos)) in test_cases.iter().enumerate() { + println!("\nTest case {}: epoch={}, fraction={}, scale={}, tz={}", + idx, epoch, fraction, scale, is_tz); + + let (struct_array, struct_field) = create_test_case(*epoch, *fraction, *scale, *is_tz); + + // Test direct function call + let dt_result = handle_snowflake_timestamp_struct(&struct_array, 0, Some(*scale)); + + // Verify result + assert!(dt_result.is_some(), + "handle_snowflake_timestamp_struct returned None for case {}", idx); + + let dt = dt_result.unwrap(); + let expected_dt = Utc.timestamp_opt(*epoch, *expected_nanos).unwrap(); + + assert_eq!(dt.timestamp(), expected_dt.timestamp(), + "Incorrect timestamp seconds for case {}", idx); + assert_eq!(dt.timestamp_subsec_nanos(), *expected_nanos, + "Incorrect nanoseconds for case {}: got {} expected {}", + idx, dt.timestamp_subsec_nanos(), expected_nanos); + + // Additionally test through handle_struct_array + let struct_array_ref = Arc::new(struct_array) as arrow::array::ArrayRef; + let result = handle_struct_array( + struct_array_ref.as_any().downcast_ref::().unwrap(), + 0, + &struct_field, + ); + + // Check result type and value + if *is_tz { + match &result { + DataType::Timestamptz(Some(result_dt)) => { + assert_eq!(result_dt.timestamp(), expected_dt.timestamp()); + assert_eq!(result_dt.timestamp_subsec_nanos(), *expected_nanos); + } + _ => panic!("Expected DataType::Timestamptz, got: {:?}", result), + } + } else { + match &result { + DataType::Timestamp(Some(result_naive_dt)) => { + assert_eq!(result_naive_dt.and_utc().timestamp(), expected_dt.timestamp()); + assert_eq!(result_naive_dt.and_utc().timestamp_subsec_nanos(), *expected_nanos); + } + _ => panic!("Expected DataType::Timestamp, got: {:?}", result), + } + } + + println!("✓ Test case {} passed", idx); + } + + println!("✓ Verified Struct Timestamps with various scales and TZ/NTZ"); + } } From d6ac872afe05be393ae1db1b23404641a565fd7f Mon Sep 17 00:00:00 2001 From: dal Date: Wed, 14 May 2025 13:21:20 -0700 Subject: [PATCH 4/4] Staging (#326) * Create a better handler for clicking favorites * chore(versions): bump api to v0.1.9; bump web to v0.1.9; bump cli to v0.1.9 [skip ci] * chore: update tag_info.json with potential release versions [skip ci] * Create a better handler for clicking favorites * update chat favorites * chore(versions): bump api to v0.1.10; bump web to v0.1.10; bump cli to v0.1.10 [skip ci] * chore: update tag_info.json with potential release versions [skip ci] * Update tests to be ran with multiple workers * create chat records update * Create createChatRecord.test.ts * chore(versions): bump api to v0.1.11; bump web to v0.1.11; bump cli to v0.1.11 [skip ci] * chore: update tag_info.json with potential release versions [skip ci] * fix yesterday bucket --------- Co-authored-by: Nate Kelley Co-authored-by: github-actions[bot] Co-authored-by: Nate Kelley <133379588+nate-kelley-buster@users.noreply.github.com> --- api/server/Cargo.toml | 2 +- cli/cli/Cargo.toml | 2 +- tag_info.json | 6 +- web/package-lock.json | 106 +- web/package.json | 18 +- web/playwright-tests/bar-chart-add-to.test.ts | 67 -- .../bar-chart-navigation.spec.ts | 132 --- .../bar-chart-styling-updates.spec.ts | 395 ------- .../bar-chart-updates.test.ts | 1002 +++++++++++++++++ .../bar-chart-x-axis-updates.spec.ts | 187 --- .../bar-chart-y-axis-updates.spec.ts | 287 ----- web/playwright-tests/chats-list.test.ts | 72 +- web/playwright-tests/collection-tests.test.ts | 150 +-- .../dashboard-updates.test.ts | 342 +++--- web/playwright-tests/invite-user.test.ts | 118 +- .../line-chart-axis-tests.spec.ts | 554 ++++----- .../metric-chart-updates.spec.ts | 134 +-- web/playwright-tests/sharing-metric.test.ts | 78 +- web/playwright.config.ts | 2 +- web/src/app/auth/layout.tsx | 2 +- .../features/sidebars/SidebarPrimary.tsx | 193 ++-- .../features/sidebars/SidebarSettings.tsx | 38 +- .../useFavoritesSidebarPanel.test.tsx | 160 +++ .../sidebars/useFavoritesSidebarPanel.tsx | 106 ++ web/src/components/ui/dropdown/Dropdown.tsx | 3 +- .../components/ui/dropdown/DropdownBase.tsx | 14 +- .../ui/list/createChatRecord.test.ts | 159 +++ .../components/ui/list/createChatRecord.ts | 70 ++ .../components/ui/list/useCreateListByDate.ts | 54 +- .../components/ui/sidebar/Sidebar.stories.tsx | 17 +- web/src/components/ui/sidebar/Sidebar.tsx | 40 +- web/src/components/ui/sidebar/interfaces.ts | 1 - 32 files changed, 2459 insertions(+), 2052 deletions(-) delete mode 100644 web/playwright-tests/bar-chart-add-to.test.ts delete mode 100644 web/playwright-tests/bar-chart-navigation.spec.ts delete mode 100644 web/playwright-tests/bar-chart-styling-updates.spec.ts create mode 100644 web/playwright-tests/bar-chart-updates.test.ts delete mode 100644 web/playwright-tests/bar-chart-x-axis-updates.spec.ts delete mode 100644 web/playwright-tests/bar-chart-y-axis-updates.spec.ts create mode 100644 web/src/components/features/sidebars/useFavoritesSidebarPanel.test.tsx create mode 100644 web/src/components/features/sidebars/useFavoritesSidebarPanel.tsx create mode 100644 web/src/components/ui/list/createChatRecord.test.ts create mode 100644 web/src/components/ui/list/createChatRecord.ts diff --git a/api/server/Cargo.toml b/api/server/Cargo.toml index 534f5301a..eb29bced4 100644 --- a/api/server/Cargo.toml +++ b/api/server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "buster_server" -version = "0.1.8" +version = "0.1.11" edition = "2021" default-run = "buster_server" diff --git a/cli/cli/Cargo.toml b/cli/cli/Cargo.toml index 8919ea60e..0a4a090a3 100644 --- a/cli/cli/Cargo.toml +++ b/cli/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "buster-cli" -version = "0.1.8" +version = "0.1.11" edition = "2021" build = "build.rs" diff --git a/tag_info.json b/tag_info.json index 5f5a0b82e..42c6a92ba 100644 --- a/tag_info.json +++ b/tag_info.json @@ -1,7 +1,7 @@ { - "api_tag": "api/v0.1.8", "api_version": "0.1.8" + "api_tag": "api/v0.1.11", "api_version": "0.1.11" , - "web_tag": "web/v0.1.8", "web_version": "0.1.8" + "web_tag": "web/v0.1.11", "web_version": "0.1.11" , - "cli_tag": "cli/v0.1.8", "cli_version": "0.1.8" + "cli_tag": "cli/v0.1.11", "cli_version": "0.1.11" } diff --git a/web/package-lock.json b/web/package-lock.json index a4fbf30d1..ed39a8010 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "web", - "version": "0.1.8", + "version": "0.1.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "web", - "version": "0.1.8", + "version": "0.1.11", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", @@ -30,11 +30,11 @@ "@radix-ui/react-tooltip": "^1.2.6", "@supabase/ssr": "^0.6.1", "@supabase/supabase-js": "^2.49.4", - "@tanstack/query-sync-storage-persister": "^5.76.0", + "@tanstack/query-sync-storage-persister": "^5.76.1", "@tanstack/react-form": "^1.11.1", - "@tanstack/react-query": "^5.76.0", - "@tanstack/react-query-devtools": "^5.76.0", - "@tanstack/react-query-persist-client": "^5.76.0", + "@tanstack/react-query": "^5.76.1", + "@tanstack/react-query-devtools": "^5.76.1", + "@tanstack/react-query-persist-client": "^5.76.1", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.8", "@types/jest": "^29.5.14", @@ -53,7 +53,7 @@ "dom-to-image": "^2.6.0", "email-validator": "^2.0.4", "font-color-contrast": "^11.1.0", - "framer-motion": "^12.11.0", + "framer-motion": "^12.11.3", "intersection-observer": "^0.12.2", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", @@ -68,7 +68,7 @@ "next-themes": "^0.4.6", "papaparse": "^5.5.2", "pluralize": "^8.0.0", - "posthog-js": "^1.240.6", + "posthog-js": "^1.242.1", "prettier": "^3.5.3", "prettier-plugin-tailwindcss": "^0.6.11", "react": "^18", @@ -76,14 +76,14 @@ "react-colorful": "^5.6.1", "react-day-picker": "8.10.1", "react-dom": "^18", - "react-hotkeys-hook": "^5.0.1", + "react-hotkeys-hook": "^5.1.0", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^15.6.1", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "sonner": "^2.0.3", "tailwind-merge": "^3.3.0", - "ts-jest": "^29.3.2", + "ts-jest": "^29.3.3", "use-context-selector": "^2.0.0", "utility-types": "^3.11.0", "valibot": "^1.1.0", @@ -6835,9 +6835,9 @@ } }, "node_modules/@tanstack/query-persist-client-core": { - "version": "5.76.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.76.0.tgz", - "integrity": "sha512-xcTZjILf4q49Nsl6wcnhBYZ4O0gpnuNwV6vPIEWIrwTuSNWz2zd/g9bc8SxnXy7xCV8SM1H0IJn8KjLQIUb2ag==", + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.76.1.tgz", + "integrity": "sha512-BkNL5QMXLwTMv5po6Ex68/xWCWdnjxx2rWeRf/we6hCI5dCN5Yf+GCbsJcSuc27I2paQJ1xFbfYs18Zmc1kJmA==", "license": "MIT", "dependencies": { "@tanstack/query-core": "5.76.0" @@ -6848,13 +6848,13 @@ } }, "node_modules/@tanstack/query-sync-storage-persister": { - "version": "5.76.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-5.76.0.tgz", - "integrity": "sha512-N8d8voY61XkM+jfXTySduLrevD6wRM3pwQ1kG0syLiWWx/sX2+CpaTMSPr0GggjQuhmjhUPo83LaV+e449tizA==", + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-5.76.1.tgz", + "integrity": "sha512-SFGP+MdZ8UDhIuD0rRI+QOVPIQF9rRJL0RzdGqGSB1i1BwhD/Gxgnyk1oMEUKQDGEWYKHjLWRVDNioGW0kSwkw==", "license": "MIT", "dependencies": { "@tanstack/query-core": "5.76.0", - "@tanstack/query-persist-client-core": "5.76.0" + "@tanstack/query-persist-client-core": "5.76.1" }, "funding": { "type": "github", @@ -6909,9 +6909,9 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.76.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.76.0.tgz", - "integrity": "sha512-dZLYzVuUFZJkenxd8o01oyFimeLBmSkaUviPHuDzXe7LSLO4WTTx92jwJlNUXOOHzg6t0XknklZ15cjhYNSDjA==", + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.76.1.tgz", + "integrity": "sha512-YxdLZVGN4QkT5YT1HKZQWiIlcgauIXEIsMOTSjvyD5wLYK8YVvKZUPAysMqossFJJfDpJW3pFn7WNZuPOqq+fw==", "license": "MIT", "dependencies": { "@tanstack/query-core": "5.76.0" @@ -6925,9 +6925,9 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "5.76.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.76.0.tgz", - "integrity": "sha512-RoyRzH5XJB//OhAdzQTutesw9uHyNZroLp/I7NDAQf8OVJKTTcoaYBmaw5pmB2e3bVdgqFu6nHFZUr5j5qBdZw==", + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.76.1.tgz", + "integrity": "sha512-LFVWgk/VtXPkerNLfYIeuGHh0Aim/k9PFGA+JxLdRaUiroQ4j4eoEqBrUpQ1Pd/KXoG4AB9vVE/M6PUQ9vwxBQ==", "license": "MIT", "dependencies": { "@tanstack/query-devtools": "5.76.0" @@ -6937,24 +6937,24 @@ "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.76.0", + "@tanstack/react-query": "^5.76.1", "react": "^18 || ^19" } }, "node_modules/@tanstack/react-query-persist-client": { - "version": "5.76.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.76.0.tgz", - "integrity": "sha512-QPKgkHX1yC1Ec21FTQHBTbQcHYI+6157DgsmxABp94H7/ZUJ3szZ7wdpdBPQyZ9VxBXlKRN+aNZkOPC90+r/uA==", + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.76.1.tgz", + "integrity": "sha512-LN8LLBDyQLTBifbL3HIAOPh48MNw2y5ff49nBUsJO6nwpl3iRKp6qwQ58rGUqHflvuAfKQKeJIvwSXMvckxJMg==", "license": "MIT", "dependencies": { - "@tanstack/query-persist-client-core": "5.76.0" + "@tanstack/query-persist-client-core": "5.76.1" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.76.0", + "@tanstack/react-query": "^5.76.1", "react": "^18 || ^19" } }, @@ -12332,12 +12332,12 @@ } }, "node_modules/framer-motion": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.11.0.tgz", - "integrity": "sha512-BaBPmkhaC2l0n619Kt1nQaxSdUdyyz5V1Z7EKJ1CcraOTZitgVx0RTbL8lmg2XesaFi6o8MPBIhkWDIvzDpGaQ==", + "version": "12.11.3", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.11.3.tgz", + "integrity": "sha512-ksUtDFBZtrbQFt4bEMFrFgo7camhmXcLeuylKQxEYSd9czkZ4tZmFROxWczWeu51WqC2m91ifpvgGCBLd0uviQ==", "license": "MIT", "dependencies": { - "motion-dom": "^12.11.0", + "motion-dom": "^12.11.2", "motion-utils": "^12.9.4", "tslib": "^2.4.0" }, @@ -16916,9 +16916,9 @@ } }, "node_modules/motion-dom": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.11.0.tgz", - "integrity": "sha512-CItkGYJenn5ZsbzTX0D9mE0UWdjdd9r535FrxEXhzR8Kwa9I2dLr1uhEJgQPWbgaIJ6i0sNFnf2T9NvVDWQVBw==", + "version": "12.11.2", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.11.2.tgz", + "integrity": "sha512-wZ396XNNTI9GOkyrr80wFSbZc1JbIHSHTbLdririSbkEgahWWKmsHzsxyxqBBvuBU/iaQWVu1YCjdpXYNfo2yQ==", "license": "MIT", "dependencies": { "motion-utils": "^12.9.4" @@ -18148,9 +18148,9 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.240.6", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.240.6.tgz", - "integrity": "sha512-Pz5r/LrMchGf9jCVnTXJrbyMhKriZRGLSZ5qt8c8QrPkmG2JOnFHNWmmBlu+iqmzbY3+oROrhwyP4IgQl2z34w==", + "version": "1.242.1", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.242.1.tgz", + "integrity": "sha512-j2mzw0eukyuw/Qm3tNZ6pfaXmc7eglWj6ftmvR1Lz9GtMr85ndGNXJvIGO+6PBrQW2o0D1G0k/KV93ehta0hFA==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "core-js": "^3.38.1", @@ -18699,9 +18699,9 @@ } }, "node_modules/react-hotkeys-hook": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-5.0.1.tgz", - "integrity": "sha512-TysTwXrUSj6QclMZIEoxCfvy/6EsoZcrfE970aUVa9fO3c3vcms+IVjv3ljbhUPM/oY1iEoun7O2W8v8INl5hw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-5.1.0.tgz", + "integrity": "sha512-GCNGXjBzV9buOS3REoQFmSmE4WTvBhYQ0YrAeeMZI83bhXg3dRWsLHXDutcVDdEjwJqJCxk5iewWYX5LtFUd7g==", "license": "MIT", "workspaces": [ "packages/*" @@ -19766,9 +19766,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -21033,9 +21033,9 @@ } }, "node_modules/ts-jest": { - "version": "29.3.2", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.2.tgz", - "integrity": "sha512-bJJkrWc6PjFVz5g2DGCNUo8z7oFEYaz1xP1NpeDU7KNLMWPpEyV8Chbpkn8xjzgRDpQhnGMyvyldoL7h8JXyug==", + "version": "29.3.3", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.3.tgz", + "integrity": "sha512-y6jLm19SL4GroiBmHwFK4dSHUfDNmOrJbRfp6QmDIlI9p5tT5Q8ItccB4pTIslCIqOZuQnBwpTR0bQ5eUMYwkw==", "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", @@ -21045,8 +21045,8 @@ "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.7.1", - "type-fest": "^4.39.1", + "semver": "^7.7.2", + "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "bin": { @@ -21082,9 +21082,9 @@ } }, "node_modules/ts-jest/node_modules/type-fest": { - "version": "4.39.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.39.1.tgz", - "integrity": "sha512-uW9qzd66uyHYxwyVBYiwS4Oi0qZyUqwjU+Oevr6ZogYiXt99EOYtwvzMSLw1c3lYo2HzJsep/NB23iEVEgjG/w==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" diff --git a/web/package.json b/web/package.json index 1bcfc96bc..b7da90768 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "web", - "version": "0.1.8", + "version": "0.1.11", "private": true, "scripts": { "dev": "next dev --turbo", @@ -42,11 +42,11 @@ "@radix-ui/react-tooltip": "^1.2.6", "@supabase/ssr": "^0.6.1", "@supabase/supabase-js": "^2.49.4", - "@tanstack/query-sync-storage-persister": "^5.76.0", + "@tanstack/query-sync-storage-persister": "^5.76.1", "@tanstack/react-form": "^1.11.1", - "@tanstack/react-query": "^5.76.0", - "@tanstack/react-query-devtools": "^5.76.0", - "@tanstack/react-query-persist-client": "^5.76.0", + "@tanstack/react-query": "^5.76.1", + "@tanstack/react-query-devtools": "^5.76.1", + "@tanstack/react-query-persist-client": "^5.76.1", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.8", "@types/jest": "^29.5.14", @@ -65,7 +65,7 @@ "dom-to-image": "^2.6.0", "email-validator": "^2.0.4", "font-color-contrast": "^11.1.0", - "framer-motion": "^12.11.0", + "framer-motion": "^12.11.3", "intersection-observer": "^0.12.2", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", @@ -80,7 +80,7 @@ "next-themes": "^0.4.6", "papaparse": "^5.5.2", "pluralize": "^8.0.0", - "posthog-js": "^1.240.6", + "posthog-js": "^1.242.1", "prettier": "^3.5.3", "prettier-plugin-tailwindcss": "^0.6.11", "react": "^18", @@ -88,14 +88,14 @@ "react-colorful": "^5.6.1", "react-day-picker": "8.10.1", "react-dom": "^18", - "react-hotkeys-hook": "^5.0.1", + "react-hotkeys-hook": "^5.1.0", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^15.6.1", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "sonner": "^2.0.3", "tailwind-merge": "^3.3.0", - "ts-jest": "^29.3.2", + "ts-jest": "^29.3.3", "use-context-selector": "^2.0.0", "utility-types": "^3.11.0", "valibot": "^1.1.0", diff --git a/web/playwright-tests/bar-chart-add-to.test.ts b/web/playwright-tests/bar-chart-add-to.test.ts deleted file mode 100644 index e49d8a8f2..000000000 --- a/web/playwright-tests/bar-chart-add-to.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('Can add to collection', async ({ page }) => { - await page.goto('http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart'); - await page.getByTestId('add-to-collection-button').click(); - await expect(page.getByRole('checkbox')).toHaveAttribute('data-state', 'unchecked'); - await page.getByRole('checkbox').click(); - await expect(page.getByRole('checkbox')).toBeVisible(); - await expect(page.getByRole('checkbox')).toHaveAttribute('data-state', 'checked'); - await page.waitForTimeout(100); - await page.waitForLoadState('networkidle'); - await page.reload(); - await page.getByTestId('add-to-collection-button').click(); - await expect(page.getByRole('checkbox')).toHaveAttribute('data-state', 'checked'); - await page.getByRole('checkbox').click(); - await expect(page.getByRole('checkbox')).toHaveAttribute('data-state', 'unchecked'); - await page.waitForTimeout(100); - await page.waitForLoadState('networkidle'); -}); - -test('Can navigate to collections page', async ({ page }) => { - await page.goto('http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart'); - await page.getByTestId('add-to-collection-button').click(); - const currentUrl = page.url(); - await page - .getByRole('menuitemcheckbox', { name: 'Important Things' }) - .getByRole('button') - .click(); - await page.goto('http://localhost:3000/app/collections/0ac43ae2-beda-4007-9574-71a17425da0a'); - expect(page.url()).not.toBe(currentUrl); -}); - -test.skip('Add to dashboard', async ({ page }) => { - await page.goto('http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart'); - await page.getByTestId('save-to-dashboard-button').click(); - await page.getByText('Important Metrics').click(); - await expect( - page.getByRole('menuitemcheckbox', { name: 'Important Metrics' }).getByRole('checkbox') - ).toBeVisible(); - - await page.waitForTimeout(100); - await page.waitForLoadState('networkidle'); - await page.reload(); - - await page.getByTestId('save-to-dashboard-button').click(); - await page - .getByRole('menuitemcheckbox', { name: 'Important Metrics' }) - .getByRole('button') - .click(); - await expect(page.getByRole('button', { name: 'Yearly Sales Revenue -' })).toBeVisible(); - await page - .locator( - 'div:nth-child(4) > .buster-resize-columns > .react-split > .react-split__pane > div > div:nth-child(2) > .bg-background > div' - ) - .first() - .click(); - await expect(page.getByRole('button', { name: 'Start chat' })).toBeVisible(); - await page.getByTestId('save-to-dashboard-button').click(); - await page - .getByRole('menuitemcheckbox', { name: 'Important Metrics' }) - .getByRole('checkbox') - .click(); - await page.getByRole('button', { name: 'Submit' }).click(); - await expect(page.getByTestId('share-button')).toBeVisible(); - await expect(page.getByTestId('three-dot-menu-button')).toBeVisible(); - await expect(page.getByRole('button', { name: 'Start chat' })).toBeVisible(); -}); diff --git a/web/playwright-tests/bar-chart-navigation.spec.ts b/web/playwright-tests/bar-chart-navigation.spec.ts deleted file mode 100644 index 71d724010..000000000 --- a/web/playwright-tests/bar-chart-navigation.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('Can click close icon in edit chart mode', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); - await page - .locator('div') - .filter({ hasText: /^Edit chart$/ }) - .getByRole('button') - .click(); - expect(page.url()).toBe( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart' - ); - await expect(page.locator('div').filter({ hasText: /^Edit chart$/ })).not.toBeVisible(); - - await page.getByTestId('edit-chart-button').getByRole('button').click(); - await expect(page.locator('div').filter({ hasText: /^Edit chart$/ })).toBeVisible(); -}); - -test('Can click start chat', async ({ page }) => { - await page.goto('http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart'); - await page.getByRole('button', { name: 'Start chat' }).click(); - await page.waitForTimeout(100); - await page.waitForLoadState('networkidle'); - await page.waitForLoadState('domcontentloaded'); - await page.waitForLoadState('load'); - await expect( - page.getByText('Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) has been') - ).toBeVisible(); - - await page.getByTestId('collapse-file-button').click(); - await expect(page.getByTestId('collapse-file-button')).not.toBeVisible({ timeout: 7000 }); - - await page.getByTestId('chat-response-message-file').click(); - await expect(page.getByTestId('metric-view-chart-content')).toBeVisible(); - await page.getByTestId('edit-chart-button').getByRole('button').click(); - await expect(page.getByText('Edit chart')).toBeVisible(); - - //CAN DELETE THE CHAT NOW - await page - .locator('div') - .filter({ hasText: /^Edit chart$/ }) - .getByRole('button') - .click(); - await page.getByTestId('chat-header-options-button').click(); - await page.getByRole('menuitem', { name: 'Delete chat' }).click(); - await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible(); - await page.getByRole('button', { name: 'Submit' }).click(); - await page.waitForTimeout(500); - - await page.waitForLoadState('networkidle'); - await page.waitForLoadState('domcontentloaded'); - await page.waitForLoadState('load'); - await page.waitForTimeout(400); - - await expect(page).toHaveURL('http://localhost:3000/app/chats', { timeout: 30000 }); -}); - -test('Can add and remove from favorites', async ({ page }) => { - await page.goto('http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart'); - await page.getByTestId('three-dot-menu-button').click(); - await page.getByRole('menuitem', { name: 'Add to favorites' }).click(); - await page.waitForTimeout(1000); - await expect(page.getByRole('link', { name: 'Yearly Sales Revenue -' })).toBeVisible(); - await page.getByTestId('three-dot-menu-button').click(); - await page.getByRole('menuitem', { name: 'Remove from favorites' }).click(); - await expect(page.getByRole('link', { name: 'Yearly Sales Revenue -' })).toBeHidden(); -}); - -test('Can open sql editor', async ({ page }) => { - await page.goto('http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart'); - await expect(page.getByTestId('segmented-trigger-sql')).toBeVisible(); - await page.getByTestId('segmented-trigger-sql').click(); - await page.waitForTimeout(55); - await page.waitForLoadState('networkidle'); - await page.waitForLoadState('domcontentloaded'); - await page.waitForLoadState('load'); - await expect(page.getByRole('button', { name: 'Run' })).toBeVisible(); - await expect(page.getByTestId('segmented-trigger-sql')).toHaveAttribute('data-state', 'active'); -}); - -test('Bar chart span clicking works', async ({ page }) => { - await page.goto('http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart'); - await page.getByTestId('edit-chart-button').getByRole('button').click(); - await page.waitForTimeout(55); - await page.getByTestId('edit-chart-button').getByRole('button').click(); - await page.waitForTimeout(55); - await page.getByTestId('edit-chart-button').getByRole('button').click(); - await page.waitForTimeout(55); - await page.getByTestId('edit-chart-button').getByRole('button').click(); - await page.waitForTimeout(55); - await page.getByTestId('segmented-trigger-sql').click(); - await page.waitForTimeout(55); - await page.getByTestId('edit-chart-button').getByRole('button').click(); - await expect(page.getByTestId('metric-view-chart-content').getByRole('img')).toBeVisible(); - await page.getByTestId('segmented-trigger-sql').click(); - await page.waitForTimeout(55); - await expect(page.getByText('Copy SQLSaveRun')).toBeVisible(); - await page.getByTestId('edit-chart-button').getByRole('button').click(); - await page.waitForLoadState('networkidle'); - await page.waitForLoadState('domcontentloaded'); - await page.waitForLoadState('load'); - - await expect(page.getByRole('textbox', { name: 'New chart' })).toBeVisible(); - await expect(page.getByRole('textbox', { name: 'New chart' })).toHaveValue( - 'Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD)' - ); - - await page.getByTestId('edit-chart-button').getByRole('button').click(); - await expect(page.getByText('Edit chart')).toBeVisible({ timeout: 15000 }); -}); - -test('Can navigate to bar chart from favorites', async ({ page }) => { - await page.goto('http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart'); - await page.getByTestId('three-dot-menu-button').click(); - await expect(page.getByText('Add to favorites')).toBeVisible(); - await page.getByRole('menuitem', { name: 'Add to favorites' }).click(); - await expect(page.getByRole('link', { name: 'Yearly Sales Revenue -' })).toBeVisible(); - await page.getByRole('link', { name: 'Home' }).click(); - await page.reload(); - await page.waitForTimeout(55); - await page.waitForLoadState('networkidle'); - await page.waitForLoadState('domcontentloaded'); - await page.waitForLoadState('load'); - await page.getByRole('link', { name: 'Yearly Sales Revenue -' }).click(); - await expect(page.getByTestId('metric-view-chart-content')).toBeVisible(); - await page.getByRole('link', { name: 'Yearly Sales Revenue -' }).getByRole('button').click(); - await page.waitForTimeout(55); - await page.waitForLoadState('networkidle'); -}); diff --git a/web/playwright-tests/bar-chart-styling-updates.spec.ts b/web/playwright-tests/bar-chart-styling-updates.spec.ts deleted file mode 100644 index 195f852fd..000000000 --- a/web/playwright-tests/bar-chart-styling-updates.spec.ts +++ /dev/null @@ -1,395 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('Can load a bar chart and remove axis', async ({ page }) => { - await page.goto('http://localhost:3000/app/home'); - await page.getByRole('link', { name: 'Metrics', exact: true }).click(); - - await page - .getByRole('link', { - name: 'Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD)' - }) - .click(); - - await expect(page.getByTestId('metric-view-chart-content')).toBeVisible(); - await expect(page.getByTestId('metric-view-chart-content').getByRole('img')).toBeVisible(); - - //can remove x axis from bar chart - await page.getByTestId('edit-chart-button').getByRole('button').click(); - await page.locator('.relative > button').first().click(); - await expect(page.getByText('No valid axis selected')).toBeVisible(); - - //can drag a numeric column to x axis - - const sourceElement = page - .getByTestId('select-axis-available-items-list') - .getByRole('button') - .first(); - expect(sourceElement).toBeVisible(); - - const targetElement = page - .getByTestId('select-axis-drop-zone-xAxis') - .locator('div') - .filter({ hasText: /^Drag column here$/ }); - expect(targetElement).toBeVisible(); - - const sourceBoundingBox = await sourceElement.boundingBox(); - const targetBoundingBox = await targetElement.boundingBox(); - - if (sourceBoundingBox && targetBoundingBox) { - // Start at the center of the source element - await page.mouse.move( - sourceBoundingBox.x + sourceBoundingBox.width / 2, - sourceBoundingBox.y + sourceBoundingBox.height / 2 - ); - await page.mouse.down(); - - // Move to target in small increments - const steps = 30; - const dx = (targetBoundingBox.x - sourceBoundingBox.x) / steps; - const dy = (targetBoundingBox.y - sourceBoundingBox.y) / steps; - - for (let i = 0; i <= steps; i++) { - await page.mouse.move( - sourceBoundingBox.x + dx * i + sourceBoundingBox.width / 2, - sourceBoundingBox.y + dy * i + sourceBoundingBox.height / 2, - { steps: 1 } - ); - await page.waitForTimeout(1); // Add a small delay between each movement - } - - await page.mouse.up(); - } - - await expect( - page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button', { name: 'Year' }) - ).toBeVisible(); - - await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(2).click(); - await expect(page.getByRole('button', { name: 'Reset' })).toBeVisible(); - await page.getByRole('button', { name: 'Reset' }).click(); - await expect(page.getByRole('button', { name: 'Reset' })).not.toBeVisible(); -}); - -test('Can add a tooltip to a bar chart', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - - const sourceElement = page - .getByTestId('select-axis-available-items-list') - .getByRole('button') - .first(); - const targetElement = page - .getByTestId('select-axis-drop-zone-tooltip') - .locator('div') - .filter({ hasText: /^Drag column here$/ }); - - const sourceBoundingBox = await sourceElement.boundingBox(); - const targetBoundingBox = await targetElement.boundingBox(); - - if (sourceBoundingBox && targetBoundingBox) { - // Start at the center of the source element - await page.mouse.move( - sourceBoundingBox.x + sourceBoundingBox.width / 2, - sourceBoundingBox.y + sourceBoundingBox.height / 2 - ); - await page.mouse.down(); - - // Move to target in small increments - const steps = 30; - const dx = (targetBoundingBox.x - sourceBoundingBox.x) / steps; - const dy = (targetBoundingBox.y - sourceBoundingBox.y) / steps; - - for (let i = 0; i <= steps; i++) { - await page.mouse.move( - sourceBoundingBox.x + dx * i + sourceBoundingBox.width / 2, - sourceBoundingBox.y + dy * i + sourceBoundingBox.height / 2, - { steps: 1 } - ); - await page.waitForTimeout(1); // Add a small delay between each movement - } - - await page.mouse.up(); - } - - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); - - page.reload(); - - await page - .getByTestId('metric-view-chart-content') - .getByRole('img') - .hover({ - position: { - x: 633, - y: 43 - } - }); - - page.reload(); - - await expect( - page.getByTestId('select-axis-drop-zone-tooltip').getByRole('button', { name: 'Year' }) - ).toBeVisible(); - await page.getByTestId('select-axis-drop-zone-tooltip').getByRole('button').nth(2).click(); - await expect( - page.getByTestId('select-axis-drop-zone-tooltip').getByText('Drag column here') - ).toBeVisible(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); -}); - -test('Can toggle legend', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - await page.getByTestId('segmented-trigger-Styling').click(); - await page - .locator('div') - .filter({ hasText: /^Show legend$/ }) - .getByRole('switch') - .click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); - - await page.reload(); - await page.getByTestId('segmented-trigger-Styling').click(); - - await page - .locator('div') - .filter({ hasText: /^Show legend$/ }) - .getByRole('switch') - .click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForLoadState('networkidle'); - await page - .locator('div') - .filter({ hasText: /^Data labels$/ }) - .getByRole('switch') - .click(); - await page - .locator('div') - .filter({ hasText: /^Data labels$/ }) - .getByRole('switch') - .click(); - await page - .locator('div') - .filter({ hasText: /^Grid lines$/ }) - .getByRole('switch') - .click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); - - await page.reload(); - await page.getByTestId('segmented-trigger-Styling').click(); - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ - - img - `); - await page - .locator('div') - .filter({ hasText: /^Grid lines$/ }) - .getByRole('switch') - .click(); - await page - .locator('div') - .filter({ hasText: /^Hide y-axis$/ }) - .getByRole('switch') - .click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(100); - await page.waitForLoadState('networkidle'); - - await page.reload(); - await page.getByTestId('segmented-trigger-Styling').click(); - - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ - - img - `); - await page - .locator('div') - .filter({ hasText: /^Hide y-axis$/ }) - .getByRole('switch') - .click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForLoadState('networkidle'); -}); - -test('Can toggle sorting', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - await page.getByTestId('segmented-trigger-Styling').click(); - await page.getByTestId('segmented-trigger-asc').click(); - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ - - img - - img - - text: Unsaved changes - - button "Reset" - - button "Save" - `); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForLoadState('networkidle'); - - await page.reload(); - await page.getByTestId('segmented-trigger-Styling').click(); - - await page.getByTestId('segmented-trigger-desc').click(); - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ - - img - - img - - text: Unsaved changes - - button "Reset" - - button "Save" - `); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); - - await page.reload(); - await page.getByTestId('segmented-trigger-Styling').click(); - - await page.getByTestId('segmented-trigger-none').click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForLoadState('networkidle'); -}); - -test('Can toggle legend headline', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - await page.getByTestId('segmented-trigger-Styling').click(); - - await page.getByRole('combobox').click(); - await page.getByRole('option', { name: 'Total' }).click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForLoadState('networkidle'); - - await page.reload(); - - await page.getByTestId('segmented-trigger-Styling').click(); - await page.getByRole('combobox').click(); - await page.getByRole('option', { name: 'None' }).click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForLoadState('networkidle'); - - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\? Total Sales Revenue/ - - img - `); -}); - -test('Can add a goal line', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - await page.getByTestId('segmented-trigger-Styling').click(); - - await page.waitForTimeout(150); - await page.getByRole('button', { name: 'Add goal line' }).click(); - - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(100); - await page.waitForLoadState('networkidle'); - - await page.reload(); - await page.getByTestId('segmented-trigger-Styling').click(); - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\? Total Sales Revenue/ - - img - `); - await page - .getByRole('main') - .filter({ hasText: 'Jan 1, 2022 - May 2, 2025•' }) - .getByRole('button') - .nth(2) - .click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); - - await page.reload(); - - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\? Total Sales Revenue/ - - img - `); -}); - -test('Can add a trendline', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - await page.getByTestId('segmented-trigger-Styling').click(); - await page.getByRole('button', { name: 'Add trend line' }).click(); - await page.getByRole('combobox').filter({ hasText: 'Linear' }).click(); - await page.getByRole('option', { name: 'Max' }).click(); - await page.getByRole('combobox').filter({ hasText: 'Max' }).click(); - await page.getByRole('option', { name: 'Median' }).click(); - await page.getByRole('combobox').filter({ hasText: 'Median' }).click(); - await page.getByRole('option', { name: 'Average' }).click(); - await page.getByRole('combobox').filter({ hasText: 'Average' }).click(); - await page.getByRole('option', { name: 'Linear' }).click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(90); - await page.waitForLoadState('networkidle'); - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - page.reload(); - - await page.getByTestId('segmented-trigger-Styling').click(); - await page.waitForTimeout(50); - await expect(page.getByTestId('trendline-Linear').locator('div').nth(1)).toBeVisible(); - await page.getByText('Styling').click(); - await page.getByTestId('trendline-Linear').locator('div').nth(1).hover(); - await page.getByTestId('delete-button').click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); -}); - -test('Can change colors', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - await page.getByTestId('segmented-trigger-Colors').click(); - await page - .locator('div') - .filter({ hasText: /^Forest Lake$/ }) - .first() - .click(); - await page.getByRole('button', { name: 'Save' }).click(); - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\? Total Sales Revenue/ - - img - `); - - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); - - await page - .locator('div') - .filter({ hasText: /^Buster$/ }) - .first() - .click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); -}); diff --git a/web/playwright-tests/bar-chart-updates.test.ts b/web/playwright-tests/bar-chart-updates.test.ts new file mode 100644 index 000000000..93e693a29 --- /dev/null +++ b/web/playwright-tests/bar-chart-updates.test.ts @@ -0,0 +1,1002 @@ +import { test, expect } from '@playwright/test'; + +test.describe.serial('Bar chart - add to tests', () => { + test('Can add to collection', async ({ page }) => { + await page.goto('http://localhost:3000/app/metrics/2b569e92-229b-5cad-b312-b09c751c544d/chart'); + await page.getByTestId('add-to-collection-button').click(); + await page.waitForLoadState('networkidle'); + await expect( + page + .getByTestId('dropdown-checkbox-0ac43ae2-beda-4007-9574-71a17425da0a') + .getByRole('checkbox') + ).toBeVisible(); + + await expect( + page + .getByTestId('dropdown-checkbox-0ac43ae2-beda-4007-9574-71a17425da0a') + .getByRole('checkbox') + ).toHaveAttribute('data-state', 'unchecked'); + await page + .getByTestId('dropdown-checkbox-0ac43ae2-beda-4007-9574-71a17425da0a') + .getByRole('checkbox') + .click(); + await page.waitForLoadState('networkidle'); + await expect( + page + .getByTestId('dropdown-checkbox-0ac43ae2-beda-4007-9574-71a17425da0a') + .getByRole('checkbox') + ).toBeVisible(); + await expect( + page + .getByTestId('dropdown-checkbox-0ac43ae2-beda-4007-9574-71a17425da0a') + .getByRole('checkbox') + ).toHaveAttribute('data-state', 'checked'); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + await page.reload(); + await page.getByTestId('add-to-collection-button').click(); + await page.waitForLoadState('networkidle'); + await expect( + page + .getByTestId('dropdown-checkbox-0ac43ae2-beda-4007-9574-71a17425da0a') + .getByRole('checkbox') + ).toHaveAttribute('data-state', 'checked'); + await page + .getByTestId('dropdown-checkbox-0ac43ae2-beda-4007-9574-71a17425da0a') + .getByRole('checkbox') + .click(); + await page.waitForLoadState('networkidle'); + await expect( + page + .getByTestId('dropdown-checkbox-0ac43ae2-beda-4007-9574-71a17425da0a') + .getByRole('checkbox') + ).toHaveAttribute('data-state', 'unchecked'); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + }); + + test('Can navigate to collections page', async ({ page }) => { + await page.goto('http://localhost:3000/app/metrics/2b569e92-229b-5cad-b312-b09c751c544d/chart'); + await page.getByTestId('add-to-collection-button').click(); + const currentUrl = page.url(); + await page + .getByRole('menuitemcheckbox', { name: 'Important Things' }) + .getByRole('button') + .click(); + await page.goto('http://localhost:3000/app/collections/0ac43ae2-beda-4007-9574-71a17425da0a'); + expect(page.url()).not.toBe(currentUrl); + }); + + test.skip('Add to dashboard', async ({ page }) => { + await page.goto('http://localhost:3000/app/metrics/2b569e92-229b-5cad-b312-b09c751c544d/chart'); + await page.getByTestId('save-to-dashboard-button').click(); + await page.getByText('Important Metrics').click(); + await expect( + page.getByRole('menuitemcheckbox', { name: 'Important Metrics' }).getByRole('checkbox') + ).toBeVisible(); + + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + await page.reload(); + + await page.getByTestId('save-to-dashboard-button').click(); + await page + .getByRole('menuitemcheckbox', { name: 'Important Metrics' }) + .getByRole('button') + .click(); + await expect(page.getByRole('button', { name: 'Yearly Sales Revenue -' })).toBeVisible(); + await page + .locator( + 'div:nth-child(4) > .buster-resize-columns > .react-split > .react-split__pane > div > div:nth-child(2) > .bg-background > div' + ) + .first() + .click(); + await expect(page.getByRole('button', { name: 'Start chat' })).toBeVisible(); + await page.getByTestId('save-to-dashboard-button').click(); + await page + .getByRole('menuitemcheckbox', { name: 'Important Metrics' }) + .getByRole('checkbox') + .click(); + await page.getByRole('button', { name: 'Submit' }).click(); + await expect(page.getByTestId('share-button')).toBeVisible(); + await expect(page.getByTestId('three-dot-menu-button')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Start chat' })).toBeVisible(); + }); +}); + +test.describe.serial('Bar chart navigation', () => { + test('Can click close icon in edit chart mode', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/9c94612e-348e-591c-bc80-fd24d556dcf7/chart?secondary_view=chart-edit' + ); + await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); + await page + .locator('div') + .filter({ hasText: /^Edit chart$/ }) + .getByRole('button') + .click(); + expect(page.url()).toBe( + 'http://localhost:3000/app/metrics/9c94612e-348e-591c-bc80-fd24d556dcf7/chart' + ); + await expect(page.locator('div').filter({ hasText: /^Edit chart$/ })).not.toBeVisible(); + + await page.getByTestId('edit-chart-button').getByRole('button').click(); + await expect(page.locator('div').filter({ hasText: /^Edit chart$/ })).toBeVisible(); + }); + + test('Can click start chat', async ({ page }) => { + await page.goto('http://localhost:3000/app/metrics/9c94612e-348e-591c-bc80-fd24d556dcf7/chart'); + await page.getByRole('button', { name: 'Start chat' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForLoadState('load'); + await expect(page.getByRole('textbox', { name: 'New chart' })).toBeVisible(); + await page.getByRole('textbox', { name: 'New chart' }).dblclick(); + await page.getByRole('textbox', { name: 'New chart' }).press('ControlOrMeta+c'); + + await expect( + page.getByText( + 'Top 10 Products by Revenue (Q2 2023 - Q1 2024) has been pulled into a new chat.' + ) + ).toBeVisible(); + + await page.getByTestId('collapse-file-button').click(); + await expect(page.getByTestId('collapse-file-button')).not.toBeVisible({ timeout: 7000 }); + + await page.getByTestId('chat-response-message-file').click(); + await expect(page.getByTestId('metric-view-chart-content')).toBeVisible(); + await page.getByTestId('edit-chart-button').getByRole('button').click(); + await expect(page.getByText('Edit chart')).toBeVisible(); + + //CAN DELETE THE CHAT NOW + await page + .locator('div') + .filter({ hasText: /^Edit chart$/ }) + .getByRole('button') + .click(); + await page.getByTestId('chat-header-options-button').click(); + await page.getByRole('menuitem', { name: 'Delete chat' }).click(); + await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible(); + await page.getByRole('button', { name: 'Submit' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForLoadState('load'); + + await expect(page).toHaveURL('http://localhost:3000/app/chats', { timeout: 30000 }); + }); + + test('Can add and remove from favorites', async ({ page }) => { + await page.goto('http://localhost:3000/app/metrics/9c94612e-348e-591c-bc80-fd24d556dcf7/chart'); + await page.getByTestId('three-dot-menu-button').click(); + await page.getByRole('menuitem', { name: 'Add to favorites' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForLoadState('load'); + + await expect(page.getByRole('link', { name: 'Top 10 Products' })).toBeVisible(); + + await page.getByTestId('three-dot-menu-button').click(); + await page.getByRole('menuitem', { name: 'Remove from favorites' }).click(); + await expect(page.getByRole('link', { name: 'Top 10 Products' })).toBeHidden(); + }); + + test('Can open sql editor', async ({ page }) => { + await page.goto('http://localhost:3000/app/metrics/9c94612e-348e-591c-bc80-fd24d556dcf7/chart'); + await expect(page.getByTestId('segmented-trigger-sql')).toBeVisible(); + await page.getByTestId('segmented-trigger-sql').click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForLoadState('load'); + await expect(page.getByRole('button', { name: 'Run' })).toBeVisible(); + await expect(page.getByTestId('segmented-trigger-sql')).toHaveAttribute('data-state', 'active'); + }); + + test('Bar chart span clicking works', async ({ page }) => { + await page.goto('http://localhost:3000/app/metrics/9c94612e-348e-591c-bc80-fd24d556dcf7/chart'); + await page.getByTestId('edit-chart-button').getByRole('button').click(); + await page.waitForTimeout(250); + await page.getByTestId('edit-chart-button').getByRole('button').click(); + await page.waitForTimeout(250); + await page.getByTestId('edit-chart-button').getByRole('button').click(); + await page.waitForTimeout(250); + await page.getByTestId('edit-chart-button').getByRole('button').click(); + await page.waitForTimeout(250); + await page.getByTestId('segmented-trigger-sql').click(); + await page.waitForTimeout(250); + await page.getByTestId('edit-chart-button').getByRole('button').click(); + await expect(page.getByTestId('metric-view-chart-content').getByRole('img')).toBeVisible(); + await page.getByTestId('segmented-trigger-sql').click(); + await page.waitForTimeout(250); + await expect(page.getByText('Copy SQLSaveRun')).toBeVisible(); + await page.getByTestId('edit-chart-button').getByRole('button').click(); + await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForLoadState('load'); + + await expect(page.getByRole('textbox', { name: 'New chart' })).toBeVisible(); + + await page.getByTestId('edit-chart-button').getByRole('button').click(); + await expect(page.getByText('Edit chart')).toBeVisible({ timeout: 15000 }); + }); + + test('Can navigate to bar chart from favorites', async ({ page }) => { + await page.goto('http://localhost:3000/app/metrics/9c94612e-348e-591c-bc80-fd24d556dcf7/chart'); + await page.getByTestId('three-dot-menu-button').click(); + await expect(page.getByText('Add to favorites')).toBeVisible(); + await page.getByRole('menuitem', { name: 'Add to favorites' }).click(); + await expect(page.getByRole('link', { name: 'Top 10 Products' })).toBeVisible(); + await page.getByRole('link', { name: 'Home' }).click(); + await page.reload(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForLoadState('load'); + await page.getByRole('link', { name: 'Top 10 Products' }).click(); + await expect(page.getByTestId('metric-view-chart-content')).toBeVisible(); + await page.getByRole('link', { name: 'Top 10 Products' }).getByRole('button').click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + }); +}); + +test.describe.serial('Bar chart styling updates', () => { + test('Can load a bar chart and remove axis', async ({ page }) => { + await page.goto('http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee'); + + await expect(page.getByTestId('metric-view-chart-content')).toBeVisible(); + await expect(page.getByTestId('metric-view-chart-content').getByRole('img')).toBeVisible(); + + //can remove x axis from bar chart + await page.getByTestId('edit-chart-button').getByRole('button').click(); + await page.locator('.relative > button').first().click(); + await expect(page.getByText('No valid axis selected')).toBeVisible(); + + //can drag a numeric column to x axis + + const sourceElement = page + .getByTestId('select-axis-available-items-list') + .getByRole('button') + .first(); + expect(sourceElement).toBeVisible(); + + const targetElement = page + .getByTestId('select-axis-drop-zone-xAxis') + .locator('div') + .filter({ hasText: /^Drag column here$/ }); + expect(targetElement).toBeVisible(); + + const sourceBoundingBox = await sourceElement.boundingBox(); + const targetBoundingBox = await targetElement.boundingBox(); + + if (sourceBoundingBox && targetBoundingBox) { + // Start at the center of the source element + await page.mouse.move( + sourceBoundingBox.x + sourceBoundingBox.width / 2, + sourceBoundingBox.y + sourceBoundingBox.height / 2 + ); + await page.mouse.down(); + + // Move to target in small increments + const steps = 30; + const dx = (targetBoundingBox.x - sourceBoundingBox.x) / steps; + const dy = (targetBoundingBox.y - sourceBoundingBox.y) / steps; + + for (let i = 0; i <= steps; i++) { + await page.mouse.move( + sourceBoundingBox.x + dx * i + sourceBoundingBox.width / 2, + sourceBoundingBox.y + dy * i + sourceBoundingBox.height / 2, + { steps: 1 } + ); + await page.waitForTimeout(1); // Add a small delay between each movement + } + + await page.mouse.up(); + } + + await expect( + page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button', { name: 'Year' }) + ).toBeVisible(); + + await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(2).click(); + await expect(page.getByRole('button', { name: 'Reset' })).toBeVisible(); + await page.getByRole('button', { name: 'Reset' }).click(); + await expect(page.getByRole('button', { name: 'Reset' })).not.toBeVisible(); + }); + + test('Can add a tooltip to a bar chart', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' + ); + + const sourceElement = page + .getByTestId('select-axis-available-items-list') + .getByRole('button') + .first(); + const targetElement = page + .getByTestId('select-axis-drop-zone-tooltip') + .locator('div') + .filter({ hasText: /^Drag column here$/ }); + + const sourceBoundingBox = await sourceElement.boundingBox(); + const targetBoundingBox = await targetElement.boundingBox(); + + if (sourceBoundingBox && targetBoundingBox) { + // Start at the center of the source element + await page.mouse.move( + sourceBoundingBox.x + sourceBoundingBox.width / 2, + sourceBoundingBox.y + sourceBoundingBox.height / 2 + ); + await page.mouse.down(); + + // Move to target in small increments + const steps = 30; + const dx = (targetBoundingBox.x - sourceBoundingBox.x) / steps; + const dy = (targetBoundingBox.y - sourceBoundingBox.y) / steps; + + for (let i = 0; i <= steps; i++) { + await page.mouse.move( + sourceBoundingBox.x + dx * i + sourceBoundingBox.width / 2, + sourceBoundingBox.y + dy * i + sourceBoundingBox.height / 2, + { steps: 1 } + ); + await page.waitForTimeout(1); // Add a small delay between each movement + } + + await page.mouse.up(); + } + + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + + page.reload(); + + await page + .getByTestId('metric-view-chart-content') + .getByRole('img') + .hover({ + position: { + x: 633, + y: 43 + } + }); + + page.reload(); + + await expect( + page.getByTestId('select-axis-drop-zone-tooltip').getByRole('button', { name: 'Year' }) + ).toBeVisible(); + await page.getByTestId('select-axis-drop-zone-tooltip').getByRole('button').nth(2).click(); + await expect( + page.getByTestId('select-axis-drop-zone-tooltip').getByText('Drag column here') + ).toBeVisible(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + }); + + test('Can toggle legend', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' + ); + await page.getByTestId('segmented-trigger-Styling').click(); + await page + .locator('div') + .filter({ hasText: /^Show legend$/ }) + .getByRole('switch') + .click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + + await page.reload(); + await page.getByTestId('segmented-trigger-Styling').click(); + + await page + .locator('div') + .filter({ hasText: /^Show legend$/ }) + .getByRole('switch') + .click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForLoadState('networkidle'); + await page + .locator('div') + .filter({ hasText: /^Data labels$/ }) + .getByRole('switch') + .click(); + await page + .locator('div') + .filter({ hasText: /^Data labels$/ }) + .getByRole('switch') + .click(); + await page + .locator('div') + .filter({ hasText: /^Grid lines$/ }) + .getByRole('switch') + .click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + + await page.reload(); + await page.getByTestId('segmented-trigger-Styling').click(); + await expect(page.locator('body')).toMatchAriaSnapshot(` + - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) + - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ + - img + `); + await page + .locator('div') + .filter({ hasText: /^Grid lines$/ }) + .getByRole('switch') + .click(); + await page + .locator('div') + .filter({ hasText: /^Hide y-axis$/ }) + .getByRole('switch') + .click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + + await page.reload(); + await page.getByTestId('segmented-trigger-Styling').click(); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) + - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ + - img + `); + await page + .locator('div') + .filter({ hasText: /^Hide y-axis$/ }) + .getByRole('switch') + .click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForLoadState('networkidle'); + }); + + test('Can toggle sorting', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' + ); + await page.getByTestId('segmented-trigger-Styling').click(); + await page.getByTestId('segmented-trigger-asc').click(); + await expect(page.locator('body')).toMatchAriaSnapshot(` + - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) + - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ + - img + - img + - text: Unsaved changes + - button "Reset" + - button "Save" + `); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForLoadState('networkidle'); + + await page.reload(); + await page.getByTestId('segmented-trigger-Styling').click(); + + await page.getByTestId('segmented-trigger-desc').click(); + await expect(page.locator('body')).toMatchAriaSnapshot(` + - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) + - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ + - img + - img + - text: Unsaved changes + - button "Reset" + - button "Save" + `); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + + await page.reload(); + await page.getByTestId('segmented-trigger-Styling').click(); + + await page.getByTestId('segmented-trigger-none').click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForLoadState('networkidle'); + }); + + test('Can toggle legend headline', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' + ); + await page.getByTestId('segmented-trigger-Styling').click(); + + await page.getByRole('combobox').click(); + await page.getByRole('option', { name: 'Total' }).click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForLoadState('networkidle'); + + await page.reload(); + + await page.getByTestId('segmented-trigger-Styling').click(); + await page.getByRole('combobox').click(); + await page.getByRole('option', { name: 'None' }).click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForLoadState('networkidle'); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) + - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\? Total Sales Revenue/ + - img + `); + }); + + test('Can add a goal line', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' + ); + await page.getByTestId('segmented-trigger-Styling').click(); + + await page.waitForTimeout(150); + await page.getByRole('button', { name: 'Add goal line' }).click(); + + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + + await page.reload(); + await page.getByTestId('segmented-trigger-Styling').click(); + await expect(page.locator('body')).toMatchAriaSnapshot(` + - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) + - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\? Total Sales Revenue/ + - img + `); + await page + .getByRole('main') + .filter({ hasText: 'Jan 1, 2022 - May 2, 2025•' }) + .getByRole('button') + .nth(2) + .click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + + await page.reload(); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) + - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\? Total Sales Revenue/ + - img + `); + }); + + test('Can add a trendline', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' + ); + await page.getByTestId('segmented-trigger-Styling').click(); + await page.getByRole('button', { name: 'Add trend line' }).click(); + await page.getByRole('combobox').filter({ hasText: 'Linear' }).click(); + await page.getByRole('option', { name: 'Max' }).click(); + await page.getByRole('combobox').filter({ hasText: 'Max' }).click(); + await page.getByRole('option', { name: 'Median' }).click(); + await page.getByRole('combobox').filter({ hasText: 'Median' }).click(); + await page.getByRole('option', { name: 'Average' }).click(); + await page.getByRole('combobox').filter({ hasText: 'Average' }).click(); + await page.getByRole('option', { name: 'Linear' }).click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(90); + await page.waitForLoadState('networkidle'); + await page.goto( + 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' + ); + page.reload(); + + await page.getByTestId('segmented-trigger-Styling').click(); + await page.waitForTimeout(50); + await expect(page.getByTestId('trendline-Linear').locator('div').nth(1)).toBeVisible(); + await page.getByText('Styling').click(); + await page.getByTestId('trendline-Linear').locator('div').nth(1).hover(); + await page.getByTestId('delete-button').click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + }); + + test('Can change colors', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' + ); + await page.getByTestId('segmented-trigger-Colors').click(); + await page + .locator('div') + .filter({ hasText: /^Forest Lake$/ }) + .first() + .click(); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.locator('body')).toMatchAriaSnapshot(` + - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) + - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\? Total Sales Revenue/ + - img + `); + + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + + await page + .locator('div') + .filter({ hasText: /^Buster$/ }) + .first() + .click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + }); +}); + +test.describe.serial('Bar chart - x axis updates', () => { + test('X axis config - Title', async ({ page }) => { + await page.goto('http://localhost:3000/app/home'); + await page.getByRole('link', { name: 'Metrics', exact: true }).click(); + + await page + .getByRole('link', { + name: 'Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD)' + }) + .click(); + + await expect(page.getByTestId('metric-view-chart-content')).toBeVisible(); + await expect(page.getByTestId('metric-view-chart-content').getByRole('img')).toBeVisible(); + + //#1 TEST WE CAN EDIT THE TITLE + await page.getByTestId('edit-chart-button').getByRole('button').click(); + await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(3).click(); + await page.getByRole('textbox', { name: 'Year' }).click(); + await page.getByRole('textbox', { name: 'Year' }).fill('WOOHOO!'); + await expect(page.getByTestId('select-axis-drop-zone-xAxis')).toContainText('WOOHOO!'); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + + await page.reload(); + await expect(page.getByTestId('select-axis-drop-zone-xAxis')).toContainText('WOOHOO!'); + await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(3).click(); + await page.getByRole('textbox', { name: 'WOOHOO!' }).click(); + await page.getByRole('textbox', { name: 'WOOHOO!' }).fill('Year'); + await page.waitForTimeout(100); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(100); + await page.reload(); + await expect(page.getByTestId('select-axis-drop-zone-xAxis')).not.toContainText('WOOHOO!'); + }); + + test('X axis config - We can edit the prefix', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/45848c7f-0d28-52a0-914e-f3fc1b7d4180/chart?secondary_view=chart-edit' + ); + await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(3).click(); + await page.getByRole('textbox', { name: '$' }).click(); + await page.getByRole('textbox', { name: '$' }).fill('SWAG'); + + await expect(page.getByRole('textbox', { name: '$' })).toHaveValue('SWAG'); + + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + await page.getByRole('textbox', { name: 'dollars' }).click(); + await page.getByRole('textbox', { name: 'dollars' }).fill('SWAG2'); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + await expect(page.getByRole('textbox', { name: '$' })).toHaveValue('SWAG'); + await expect(page.getByRole('textbox', { name: 'dollars' })).toHaveValue('SWAG2'); + + await page.reload(); + + await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(3).click(); + await page.getByRole('textbox', { name: '$' }).click(); + await page.getByRole('textbox', { name: '$' }).fill(''); + await page.getByRole('textbox', { name: 'dollars' }).click(); + await page.getByRole('textbox', { name: 'dollars' }).fill(''); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(150); + await page.waitForLoadState('networkidle'); + }); +}); + +test.describe.serial('Bar chart - y axis updates', () => { + test('Y axis config - Title', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' + ); + await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); + await page.getByRole('textbox', { name: 'Total Sales Revenue' }).click(); + await page.getByRole('textbox', { name: 'Total Sales Revenue' }).press('ControlOrMeta+a'); + await page.getByRole('textbox', { name: 'Total Sales Revenue' }).fill('THIS IS A TEST!'); + await expect(page.getByRole('button', { name: 'THIS IS A TEST!' })).toBeVisible(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + expect(page.getByRole('textbox', { name: 'THIS IS A TEST!' })).toBeVisible(); + + await page.reload(); + await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); + await page.getByRole('textbox', { name: 'THIS IS A TEST!' }).click(); + await page.getByRole('textbox', { name: 'THIS IS A TEST!' }).fill('Total Sales Revenue'); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + + await page.reload(); + await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); + await expect(page.getByRole('textbox', { name: 'Total Sales Revenue' })).toBeVisible(); + }); + + test('Y axis config - Label style', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' + ); + await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); + await page.getByTestId('segmented-trigger-percent').click(); + await expect(page.getByText('Unsaved changes')).toBeVisible(); + await page.waitForTimeout(100); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForLoadState('networkidle'); + + await page.reload(); + + await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); + expect(page.getByTestId('segmented-trigger-percent')).toHaveAttribute('data-state', 'active'); + + await page.getByTestId('segmented-trigger-number').click(); + + await expect(page.getByText('Unsaved changes')).toBeVisible(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + }); + + test('Y axis config - Label seperator style', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' + ); + await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); + await page.getByTestId('edit-separator-input').getByRole('combobox').click(); + expect(page.getByRole('option', { name: '100000' })).toBeVisible(); + await page.getByRole('option', { name: '100000' }).click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForLoadState('networkidle'); + await page.getByTestId('edit-separator-input').getByRole('combobox').click(); + await page.getByRole('option', { name: '100,000' }).click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + + await page.reload(); + await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); + expect(page.getByText('100,000')).toBeVisible(); + }); + + test('Y axis config - adjust bar roundness', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' + ); + await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); + await page.getByRole('slider').click(); + await page + .locator('div') + .filter({ hasText: /^Bar roundness$/ }) + .getByRole('spinbutton') + .fill('25'); + await page.getByRole('textbox', { name: 'Total Sales Revenue' }).click(); + await page + .locator('div') + .filter({ hasText: /^TitleBar roundnessShow data labels$/ }) + .getByRole('spinbutton') + .click(); + await page + .locator('div') + .filter({ hasText: /^TitleBar roundnessShow data labels$/ }) + .getByRole('spinbutton') + .fill('26'); + await page + .locator('div') + .filter({ hasText: /^TitleBar roundnessShow data labels$/ }) + .getByRole('spinbutton') + .press('Enter'); + await expect( + page + .locator('div') + .filter({ hasText: /^Bar roundness$/ }) + .getByRole('spinbutton') + ).toBeVisible(); + await expect(page.getByRole('button', { name: 'Save' })).toBeVisible(); + + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + + await page.reload(); + await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); + + await page + .locator('div') + .filter({ hasText: /^Bar roundness$/ }) + .getByRole('spinbutton') + .click(); + await page + .locator('div') + .filter({ hasText: /^Bar roundness$/ }) + .getByRole('spinbutton') + .fill('8'); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + await expect(page.locator('body')).toMatchAriaSnapshot(` + - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) + - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ + - img + `); + // + }); + + test('Y axis config - show data labels', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' + ); + await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); + await page.getByRole('switch').click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); + await expect(page.locator('body')).toMatchAriaSnapshot(` + - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) + - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ + - img + `); + await page + .locator('div') + .filter({ hasText: /^Show label as %$/ }) + .getByRole('switch') + .click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + + await page.reload(); + await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) + - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ + - img + `); + await page + .locator('div') + .filter({ hasText: /^Show label as %$/ }) + .getByRole('switch') + .click(); + await page + .locator('div') + .filter({ hasText: /^Show data labels$/ }) + .getByRole('switch') + .click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + + await page.reload(); + await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) + - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ + - img + `); + }); + + test('Y axis config - global settings', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' + ); + await page + .locator('div') + .filter({ hasText: /^Y-Axis$/ }) + .getByRole('button') + .click(); + await page + .locator('div') + .filter({ hasText: /^Show axis title$/ }) + .getByRole('switch') + .click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) + - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ + - img + `); + + await page.reload(); + + await page + .locator('div') + .filter({ hasText: /^Y-Axis$/ }) + .getByRole('button') + .click(); + await page + .locator('div') + .filter({ hasText: /^Show axis title$/ }) + .getByRole('switch') + .click(); + + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + await expect(page.locator('body')).toMatchAriaSnapshot(` + - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) + - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ + - img + `); + + await page.reload(); + + await page + .locator('div') + .filter({ hasText: /^Y-Axis$/ }) + .getByRole('button') + .click(); + await page + .locator('div') + .filter({ hasText: /^Show axis label$/ }) + .getByRole('switch') + .click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) + - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ + - img + `); + + await page.reload(); + + await page + .locator('div') + .filter({ hasText: /^Y-Axis$/ }) + .getByRole('button') + .click(); + await page + .locator('div') + .filter({ hasText: /^Show axis label$/ }) + .getByRole('switch') + .click(); + await page.getByRole('combobox').click(); + await page.getByRole('option', { name: 'Logarithmic' }).click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) + - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ + - img + `); + await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); + await page + .locator('div') + .filter({ hasText: /^Y-Axis$/ }) + .getByRole('button') + .click(); + await page.getByRole('combobox').filter({ hasText: 'Logarithmic' }).click(); + await page.getByRole('option', { name: 'Linear' }).click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + }); +}); diff --git a/web/playwright-tests/bar-chart-x-axis-updates.spec.ts b/web/playwright-tests/bar-chart-x-axis-updates.spec.ts deleted file mode 100644 index 2fc1197ad..000000000 --- a/web/playwright-tests/bar-chart-x-axis-updates.spec.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('X axis config - Title', async ({ page }) => { - await page.goto('http://localhost:3000/app/home'); - await page.getByRole('link', { name: 'Metrics', exact: true }).click(); - - await page - .getByRole('link', { - name: 'Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD)' - }) - .click(); - - await expect(page.getByTestId('metric-view-chart-content')).toBeVisible(); - await expect(page.getByTestId('metric-view-chart-content').getByRole('img')).toBeVisible(); - - //#1 TEST WE CAN EDIT THE TITLE - await page.getByTestId('edit-chart-button').getByRole('button').click(); - await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(3).click(); - await page.getByRole('textbox', { name: 'Year' }).click(); - await page.getByRole('textbox', { name: 'Year' }).fill('WOOHOO!'); - await expect(page.getByTestId('select-axis-drop-zone-xAxis')).toContainText('WOOHOO!'); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); - - await page.reload(); - await expect(page.getByTestId('select-axis-drop-zone-xAxis')).toContainText('WOOHOO!'); - await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(3).click(); - await page.getByRole('textbox', { name: 'WOOHOO!' }).click(); - await page.getByRole('textbox', { name: 'WOOHOO!' }).fill('Year'); - await page.waitForTimeout(100); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); - await page.waitForTimeout(100); - await page.reload(); - await expect(page.getByTestId('select-axis-drop-zone-xAxis')).not.toContainText('WOOHOO!'); -}); - -test('X axis config - we can edit the label style', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - - await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(3).click(); - await page.getByTestId('segmented-trigger-percent').click(); - await expect(page.getByText('Unsaved changes')).toBeVisible(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(100); - await page.waitForLoadState('networkidle'); - await page.reload(); - await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(3).click(); - await page.getByTestId('segmented-trigger-number').click(); - await expect(page.getByText('Unsaved changes')).toBeVisible(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(100); - await page.waitForLoadState('networkidle'); - await page.reload(); - await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(3).click(); - expect(page.getByTestId('segmented-trigger-number')).toHaveAttribute('data-state', 'active'); -}); - -test('X axis config - We can edit the label separator style', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(3).click(); - await page.getByRole('combobox').click(); - await page.getByRole('option', { name: '100,000' }).click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); - - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ - - img - `); - await page.waitForTimeout(20); - await page.waitForLoadState('networkidle'); - - await page.reload(); - await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(3).click(); - await page.getByRole('combobox').click(); - await page.getByRole('option', { name: '100000' }).click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); - await page.reload(); - await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(3).click(); - - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ - - img - `); -}); - -test('X axis config - We can edit the decimal places', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(3).click(); - await page.getByRole('spinbutton').first().click(); - await page.getByRole('spinbutton').first().fill('2'); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); - - await page.reload(); - await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(3).click(); - expect(page.getByRole('spinbutton').first()).toHaveValue('2'); - - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ - - img - `); - await page.getByRole('spinbutton').first().click(); - await page.getByRole('spinbutton').first().fill('0'); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); -}); - -test('X axis config - We can edit the multiply by places', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(3).click(); - await page.getByPlaceholder('1').click(); - await page.getByPlaceholder('1').fill('10'); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); - - await page.reload(); - await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(3).click(); - expect(page.getByPlaceholder('1')).toHaveValue('10'); - await page.getByPlaceholder('1').click(); - await page.getByPlaceholder('1').fill('1'); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); -}); - -test('X axis config - We can edit the prefix', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(3).click(); - await page.getByRole('textbox', { name: '$' }).click(); - await page.getByRole('textbox', { name: '$' }).fill('SWAG'); - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ - - img - - img - - text: Unsaved changes - - button "Reset" - - button "Save" - `); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); - await page.getByRole('textbox', { name: 'dollars' }).click(); - await page.getByRole('textbox', { name: 'dollars' }).fill('SWAG2'); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ - - img - `); - - await page.reload(); - - await page.getByTestId('select-axis-drop-zone-xAxis').getByRole('button').nth(3).click(); - await page.getByRole('textbox', { name: '$' }).click(); - await page.getByRole('textbox', { name: '$' }).fill(''); - await page.getByRole('textbox', { name: 'dollars' }).click(); - await page.getByRole('textbox', { name: 'dollars' }).fill(''); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); -}); diff --git a/web/playwright-tests/bar-chart-y-axis-updates.spec.ts b/web/playwright-tests/bar-chart-y-axis-updates.spec.ts deleted file mode 100644 index f3589ec48..000000000 --- a/web/playwright-tests/bar-chart-y-axis-updates.spec.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('Y axis config - Title', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); - await page.getByRole('textbox', { name: 'Total Sales Revenue' }).click(); - await page.getByRole('textbox', { name: 'Total Sales Revenue' }).press('ControlOrMeta+a'); - await page.getByRole('textbox', { name: 'Total Sales Revenue' }).fill('THIS IS A TEST!'); - await expect(page.getByRole('button', { name: 'THIS IS A TEST!' })).toBeVisible(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); - expect(page.getByRole('textbox', { name: 'THIS IS A TEST!' })).toBeVisible(); - - await page.reload(); - await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); - await page.getByRole('textbox', { name: 'THIS IS A TEST!' }).click(); - await page.getByRole('textbox', { name: 'THIS IS A TEST!' }).fill('Total Sales Revenue'); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); - - await page.reload(); - await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); - await expect(page.getByRole('textbox', { name: 'Total Sales Revenue' })).toBeVisible(); -}); - -test('Y axis config - Label style', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); - await page.getByTestId('segmented-trigger-percent').click(); - await expect(page.getByText('Unsaved changes')).toBeVisible(); - await page.waitForTimeout(250); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForLoadState('networkidle'); - - await page.reload(); - - await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); - expect(page.getByTestId('segmented-trigger-percent')).toHaveAttribute('data-state', 'active'); - - await page.getByTestId('segmented-trigger-number').click(); - - await expect(page.getByText('Unsaved changes')).toBeVisible(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); -}); - -test('Y axis config - Label seperator style', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); - await page.getByTestId('edit-separator-input').getByRole('combobox').click(); - expect(page.getByRole('option', { name: '100000' })).toBeVisible(); - await page.getByRole('option', { name: '100000' }).click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForLoadState('networkidle'); - await page.getByTestId('edit-separator-input').getByRole('combobox').click(); - await page.getByRole('option', { name: '100,000' }).click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); - - await page.reload(); - await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); - expect(page.getByText('100,000')).toBeVisible(); -}); - -test('Y axis config - adjust bar roundness', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); - await page.getByRole('slider').click(); - await page - .locator('div') - .filter({ hasText: /^Bar roundness$/ }) - .getByRole('spinbutton') - .fill('25'); - await expect( - page - .locator('div') - .filter({ hasText: /^Bar roundness$/ }) - .getByRole('spinbutton') - ).toBeVisible(); - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ - - img - - img - - text: Unsaved changes - - button "Reset" - - button "Save" - `); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); - - await page.reload(); - await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); - - await page - .locator('div') - .filter({ hasText: /^Bar roundness$/ }) - .getByRole('spinbutton') - .click(); - await page - .locator('div') - .filter({ hasText: /^Bar roundness$/ }) - .getByRole('spinbutton') - .fill('8'); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ - - img - `); - // -}); - -test('Y axis config - show data labels', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); - await page.getByRole('switch').click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(100); - await page.waitForLoadState('networkidle'); - await page.waitForLoadState('domcontentloaded'); - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ - - img - `); - await page - .locator('div') - .filter({ hasText: /^Show label as %$/ }) - .getByRole('switch') - .click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); - - await page.reload(); - await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); - - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ - - img - `); - await page - .locator('div') - .filter({ hasText: /^Show label as %$/ }) - .getByRole('switch') - .click(); - await page - .locator('div') - .filter({ hasText: /^Show data labels$/ }) - .getByRole('switch') - .click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); - - await page.reload(); - await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); - - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ - - img - `); -}); - -test('Y axis config - global settings', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/45c17750-2b61-5683-ba8d-ff6c6fefacee/chart?secondary_view=chart-edit' - ); - await page - .locator('div') - .filter({ hasText: /^Y-Axis$/ }) - .getByRole('button') - .click(); - await page - .locator('div') - .filter({ hasText: /^Show axis title$/ }) - .getByRole('switch') - .click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); - - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ - - img - `); - - await page.reload(); - - await page - .locator('div') - .filter({ hasText: /^Y-Axis$/ }) - .getByRole('button') - .click(); - await page - .locator('div') - .filter({ hasText: /^Show axis title$/ }) - .getByRole('switch') - .click(); - - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ - - img - `); - - await page.reload(); - - await page - .locator('div') - .filter({ hasText: /^Y-Axis$/ }) - .getByRole('button') - .click(); - await page - .locator('div') - .filter({ hasText: /^Show axis label$/ }) - .getByRole('switch') - .click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); - - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ - - img - `); - - await page.reload(); - - await page - .locator('div') - .filter({ hasText: /^Y-Axis$/ }) - .getByRole('button') - .click(); - await page - .locator('div') - .filter({ hasText: /^Show axis label$/ }) - .getByRole('switch') - .click(); - await page.getByRole('combobox').click(); - await page.getByRole('option', { name: 'Logarithmic' }).click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); - - await expect(page.locator('body')).toMatchAriaSnapshot(` - - textbox "New chart": Yearly Sales Revenue - Signature Cycles Products (Last 3 Years + YTD) - - text: /Jan 1, \\d+ - May 2, \\d+ • What is the total yearly sales revenue for products supplied by Signature Cycles from \\d+ to present\\?/ - - img - `); - await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); - await page - .locator('div') - .filter({ hasText: /^Y-Axis$/ }) - .getByRole('button') - .click(); - await page.getByRole('combobox').filter({ hasText: 'Logarithmic' }).click(); - await page.getByRole('option', { name: 'Linear' }).click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(250); - await page.waitForLoadState('networkidle'); -}); diff --git a/web/playwright-tests/chats-list.test.ts b/web/playwright-tests/chats-list.test.ts index e5067bebf..efefbc9fb 100644 --- a/web/playwright-tests/chats-list.test.ts +++ b/web/playwright-tests/chats-list.test.ts @@ -5,42 +5,44 @@ const chatsRoute = createBusterRoute({ route: BusterRoutes.APP_CHAT }); -test('Can navigate to a chat from the chat history list', async ({ page }) => { - await page.goto('http://localhost:3000/app/home'); - await page.getByRole('link', { name: 'Chat history' }).click(); - await expect(page.getByText('Chat history')).toBeVisible(); +test.describe.serial('Chats list', () => { + test('Can navigate to a chat from the chat history list', async ({ page }) => { + await page.goto('http://localhost:3000/app/home'); + await page.getByRole('link', { name: 'Chat history' }).click(); + await expect(page.getByText('Chat history')).toBeVisible(); - await page.locator('.list-container').getByRole('link').first().click(); + await page.locator('.list-container').getByRole('link').first().click(); - await page.waitForURL((url) => url.toString() !== chatsRoute); - expect(page.url()).not.toEqual(chatsRoute); -}); - -test('Complex click through for a chat', async ({ page }) => { - await page.goto('http://localhost:3000/app/chats'); - await page.getByRole('link', { name: 'Total Products Sold To Date' }).click(); - await expect(page.getByTestId('chat-response-message-file')).toBeVisible(); - await page.getByRole('link', { name: 'Chat history' }).click(); - await page.getByRole('link', { name: 'Top Customer Identification' }).click(); - await page.goto('http://localhost:3000/app/chats/0ba71c06-f86d-4a2d-973c-3870e8a5372e'); - await page.getByRole('textbox', { name: 'Ask Buster a question...' }).click(); - await page.getByRole('link', { name: 'Chat history' }).click(); - await page.getByRole('link', { name: 'Most Active Vendor Last 3' }).click(); - await expect(page.getByTestId('metric-view-chart-content').getByRole('img')).toBeVisible(); - await page.getByTestId('collapse-file-button').click(); - await expect(page.getByTestId('metric-view-chart-content').getByRole('img')).not.toBeVisible({ - timeout: 1000 + await page.waitForURL((url) => url.toString() !== chatsRoute); + expect(page.url()).not.toEqual(chatsRoute); + }); + + test('Complex click through for a chat', async ({ page }) => { + await page.goto('http://localhost:3000/app/chats'); + await page.getByRole('link', { name: 'Total Products Sold To Date' }).click(); + await expect(page.getByTestId('chat-response-message-file')).toBeVisible(); + await page.getByRole('link', { name: 'Chat history' }).click(); + await page.getByRole('link', { name: 'Top Customer Identification' }).click(); + await page.goto('http://localhost:3000/app/chats/0ba71c06-f86d-4a2d-973c-3870e8a5372e'); + await page.getByRole('textbox', { name: 'Ask Buster a question...' }).click(); + await page.getByRole('link', { name: 'Chat history' }).click(); + await page.getByRole('link', { name: 'Most Active Vendor Last 3' }).click(); + await expect(page.getByTestId('metric-view-chart-content').getByRole('img')).toBeVisible(); + await page.getByTestId('collapse-file-button').click(); + await expect(page.getByTestId('metric-view-chart-content').getByRole('img')).not.toBeVisible({ + timeout: 1000 + }); + await page.getByTestId('chat-response-message-file').click(); + await expect(page.getByTestId('metric-view-chart-content').getByRole('img')).toBeVisible(); + await page.getByTestId('edit-chart-button').getByRole('button').click(); + await expect(page.getByTestId('metric-view-chart-content').getByRole('img')).toBeVisible(); + await expect(page.getByText('Edit chart')).toBeVisible(); + await page + .locator('div') + .filter({ hasText: /^Edit chart$/ }) + .getByRole('button') + .click(); + await expect(page.getByTestId('chat-response-message-file')).toBeVisible(); + await expect(page.getByText('Edit chart')).not.toBeVisible(); }); - await page.getByTestId('chat-response-message-file').click(); - await expect(page.getByTestId('metric-view-chart-content').getByRole('img')).toBeVisible(); - await page.getByTestId('edit-chart-button').getByRole('button').click(); - await expect(page.getByTestId('metric-view-chart-content').getByRole('img')).toBeVisible(); - await expect(page.getByText('Edit chart')).toBeVisible(); - await page - .locator('div') - .filter({ hasText: /^Edit chart$/ }) - .getByRole('button') - .click(); - await expect(page.getByTestId('chat-response-message-file')).toBeVisible(); - await expect(page.getByText('Edit chart')).not.toBeVisible(); }); diff --git a/web/playwright-tests/collection-tests.test.ts b/web/playwright-tests/collection-tests.test.ts index ce62d0cc7..d086a7e3a 100644 --- a/web/playwright-tests/collection-tests.test.ts +++ b/web/playwright-tests/collection-tests.test.ts @@ -1,78 +1,80 @@ import { test, expect } from '@playwright/test'; -test('Can create a collection', async ({ page }) => { - await page.goto('http://localhost:3000/app/collections'); - await page.getByRole('button', { name: 'New Collection' }).click(); - await page.getByRole('textbox', { name: 'Collection title' }).click(); - await page.getByRole('textbox', { name: 'Collection title' }).fill('My cool test'); - await page.getByRole('button', { name: 'Create collection' }).click(); +test.describe.serial('collection tests', () => { + test('Can create a collection', async ({ page }) => { + await page.goto('http://localhost:3000/app/collections'); + await page.getByRole('button', { name: 'New Collection' }).click(); + await page.getByRole('textbox', { name: 'Collection title' }).click(); + await page.getByRole('textbox', { name: 'Collection title' }).fill('My cool test'); + await page.getByRole('button', { name: 'Create collection' }).click(); - await expect( - page.getByRole('main').getByRole('button', { name: 'Add to collection' }) - ).toBeVisible(); - await page.getByRole('button').filter({ hasText: /^$/ }).nth(2).click(); - await page.getByRole('menuitem', { name: 'Rename collection' }).click(); - await page.getByRole('textbox', { name: 'Enter collection name' }).fill('Nate rulez!'); - await page.getByRole('button', { name: 'Rename' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); - await page.reload(); - await expect(page.getByRole('link', { name: 'Nate rulez!' })).toBeVisible(); - await page.getByRole('button').filter({ hasText: /^$/ }).nth(2).click(); - await page.getByRole('menuitem', { name: 'Delete collection' }).click(); - await page.getByRole('button', { name: 'Submit' }).click(); - await page.waitForTimeout(1000); - await page.waitForLoadState('networkidle'); - await page.waitForLoadState('domcontentloaded'); - await page.waitForLoadState('load'); - expect(page.url()).toBe('http://localhost:3000/app/collections'); -}); - -test('Can add a metric to a collection', async ({ page }) => { - await page.goto('http://localhost:3000/app/collections'); - await page.getByRole('link', { name: 'Important Things' }).click(); - await page.getByRole('button', { name: 'Add to collection' }).click(); - await page.waitForTimeout(550); - - await page.getByText('Yearly Sales Revenue -').click(); - - await page.getByRole('button', { name: 'Add assets' }).click(); - await expect(page.getByRole('link', { name: 'Yearly Sales Revenue -' })).toBeVisible(); - await expect(page.getByRole('link', { name: 'Quarterly Revenue Report (' })).toBeVisible(); - await expect(page.getByRole('link', { name: 'Quarterly Revenue Growth Rate' })).toBeVisible(); - await page.getByRole('button', { name: 'Add to collection' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); - await page - .getByLabel('Input Modal') - .locator('div') - .filter({ - hasText: /^Yearly Sales Revenue - Signature Cycles Products \(Last 3 Years \+ YTD\)$/ - }) - .first() - .click(); - await page.getByRole('button', { name: 'Remove assets' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); - await expect(page.getByRole('link', { name: 'Yearly Sales Revenue -' })).toBeHidden(); -}); - -test('Complex collection click through', async ({ page }) => { - await page.goto('http://localhost:3000/app/collections/0ac43ae2-beda-4007-9574-71a17425da0a'); - await page.getByRole('link', { name: 'Quarterly Revenue Growth Rate' }).click(); - await expect(page.getByTestId('metric-view-chart-content').getByRole('img')).toBeVisible(); - await page.goBack(); - await expect(page.getByRole('link', { name: 'Important Things' })).toBeVisible(); - await page.waitForTimeout(650); - await page.getByRole('link', { name: 'Revenue by Sales Territory (' }).click(); - await page.getByRole('button', { name: 'Start chat' }).click(); - await page.waitForTimeout(650); - await page.waitForLoadState('networkidle'); - await expect(page.getByTestId('chat-response-message-file')).toBeVisible(); - await page.waitForTimeout(650); - await page.goBack(); - await page.waitForTimeout(650); - await page.goBack(); - await page.waitForTimeout(650); - await expect(page.getByRole('textbox', { name: 'New chart' })).not.toBeVisible(); + await expect( + page.getByRole('main').getByRole('button', { name: 'Add to collection' }) + ).toBeVisible(); + await page.getByRole('button').filter({ hasText: /^$/ }).nth(2).click(); + await page.getByRole('menuitem', { name: 'Rename collection' }).click(); + await page.getByRole('textbox', { name: 'Enter collection name' }).fill('Nate rulez!'); + await page.getByRole('button', { name: 'Rename' }).click(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + await page.reload(); + await expect(page.getByRole('link', { name: 'Nate rulez!' })).toBeVisible(); + await page.getByRole('button').filter({ hasText: /^$/ }).nth(2).click(); + await page.getByRole('menuitem', { name: 'Delete collection' }).click(); + await page.getByRole('button', { name: 'Submit' }).click(); + await page.waitForTimeout(1000); + await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForLoadState('load'); + expect(page.url()).toBe('http://localhost:3000/app/collections'); + }); + + test('Can add a metric to a collection', async ({ page }) => { + await page.goto('http://localhost:3000/app/collections'); + await page.getByRole('link', { name: 'Important Things' }).click(); + await page.getByRole('button', { name: 'Add to collection' }).click(); + await page.waitForTimeout(550); + + await page.getByText('Yearly Sales Revenue -').click(); + + await page.getByRole('button', { name: 'Add assets' }).click(); + await expect(page.getByRole('link', { name: 'Yearly Sales Revenue -' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Quarterly Revenue Report (' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Quarterly Revenue Growth Rate' })).toBeVisible(); + await page.getByRole('button', { name: 'Add to collection' }).click(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + await page + .getByLabel('Input Modal') + .locator('div') + .filter({ + hasText: /^Yearly Sales Revenue - Signature Cycles Products \(Last 3 Years \+ YTD\)$/ + }) + .first() + .click(); + await page.getByRole('button', { name: 'Remove assets' }).click(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + await expect(page.getByRole('link', { name: 'Yearly Sales Revenue -' })).toBeHidden(); + }); + + test('Complex collection click through', async ({ page }) => { + await page.goto('http://localhost:3000/app/collections/0ac43ae2-beda-4007-9574-71a17425da0a'); + await page.getByRole('link', { name: 'Quarterly Revenue Growth Rate' }).click(); + await expect(page.getByTestId('metric-view-chart-content').getByRole('img')).toBeVisible(); + await page.goBack(); + await expect(page.getByRole('link', { name: 'Important Things' })).toBeVisible(); + await page.waitForTimeout(650); + await page.getByRole('link', { name: 'Revenue by Sales Territory (' }).click(); + await page.getByRole('button', { name: 'Start chat' }).click(); + await page.waitForTimeout(650); + await page.waitForLoadState('networkidle'); + await expect(page.getByTestId('chat-response-message-file')).toBeVisible(); + await page.waitForTimeout(650); + await page.goBack(); + await page.waitForTimeout(650); + await page.goBack(); + await page.waitForTimeout(650); + await expect(page.getByRole('textbox', { name: 'New chart' })).not.toBeVisible(); + }); }); diff --git a/web/playwright-tests/dashboard-updates.test.ts b/web/playwright-tests/dashboard-updates.test.ts index f1bee1b8d..6292494c3 100644 --- a/web/playwright-tests/dashboard-updates.test.ts +++ b/web/playwright-tests/dashboard-updates.test.ts @@ -1,173 +1,177 @@ import { test, expect } from '@playwright/test'; -test('Go to dashboard', async ({ page }) => { - await page.goto('http://localhost:3000/app/dashboards'); - await expect(page.getByText('Dashboards').nth(1)).toBeVisible(); - await expect(page.getByRole('button', { name: 'New dashboard' })).toBeVisible(); - await expect(page.getByRole('button', { name: '12px star' })).toBeVisible(); - await page.getByRole('link', { name: 'Important Metrics 12px star' }).click(); - await page.waitForTimeout(100); - await page.waitForLoadState('networkidle'); - await page.waitForLoadState('domcontentloaded'); - await expect(page.getByRole('textbox', { name: 'New dashboard' })).toHaveValue( - 'Important Metrics' - ); -}); - -test('Can remove a metric from a dashboard', async ({ page }) => { - await page.goto('http://localhost:3000/app/dashboards/c0855f0f-f50a-424e-9e72-9e53711a7f6a'); - await expect(page.getByRole('button', { name: 'Quarterly Gross Profit Margin' })).toBeVisible(); - // Hover over the metric to reveal the three-dot menu - await page - .locator(`[data-testid="metric-item-72e445a5-fb08-5b76-8c77-1642adf0cb72"]`) - .hover({ timeout: 500 }); - await expect( - page.locator(`[data-testid="metric-item-72e445a5-fb08-5b76-8c77-1642adf0cb72"]`) - ).toBeVisible(); - - await page - .getByTestId('metric-item-72e445a5-fb08-5b76-8c77-1642adf0cb72') - .locator('button') - .click(); - await page.getByRole('menuitem', { name: 'Delete' }).click(); - await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible(); - await page.getByRole('button', { name: 'Submit' }).click(); - await expect( - page.getByRole('button', { name: 'Quarterly Gross Profit Margin' }) - ).not.toBeVisible(); - await page - .locator('div') - .filter({ hasText: /^DashboardFileStart chat$/ }) - .getByRole('button') - .nth(2) - .click(); - await page.getByRole('textbox', { name: 'Search...' }).click(); - await page - .getByRole('textbox', { name: 'Search...' }) - .fill('Quarterly Gross Profit Margin Trend (Q2 2023 - Q1 2024)'); - await page.waitForTimeout(450); - await page.waitForLoadState('networkidle'); - await page - .locator('div:nth-child(2) > div > div > div > .border-border > div > .peer') - .first() - .click(); - await expect(page.getByRole('button', { name: 'Add metrics' })).toBeVisible(); - await page.getByRole('button', { name: 'Add metrics' }).click(); - await page.waitForTimeout(300); - await page.waitForLoadState('networkidle'); - await expect(page.getByTestId('metric-item-72e445a5-fb08-5b76-8c77-1642adf0cb72')).toBeVisible(); - - //drag back into place - // Use a more specific selector if the element is a link - const sourceElement = page.getByRole('button', { name: 'Quarterly Gross Profit Margin' }); - const targetElement = page.getByRole('button', { name: 'Revenue by Product Category (' }); - - expect(sourceElement).toBeVisible(); - expect(targetElement).toBeVisible(); - - try { - // Skip the initial click since it's a link and would navigate away - // Go straight to hover and mouse operations - await sourceElement.hover({ force: true }); - await page.waitForTimeout(200); - await page.mouse.down(); - await page.waitForTimeout(200); - - // Move to target element - await targetElement.hover({ force: true, position: { x: 5, y: 5 } }); - await page.waitForTimeout(400); - - await page.mouse.up(); - await page.waitForTimeout(1000); - } catch (e) { - const sourceBoundingBox = await sourceElement.boundingBox(); - const targetBoundingBox = await targetElement.boundingBox(); - - if (sourceBoundingBox && targetBoundingBox) { - const startX = sourceBoundingBox.x + sourceBoundingBox.width / 2; - const startY = sourceBoundingBox.y + sourceBoundingBox.height / 2; - const endX = targetBoundingBox.x + 10; - const endY = targetBoundingBox.y + targetBoundingBox.height / 2; - - // Skip the initial click - await page.mouse.move(startX, startY); - await page.waitForTimeout(300); - await page.mouse.down(); - await page.waitForTimeout(300); - - // Move to destination with a slower motion - const steps = 20; - for (let i = 0; i <= steps; i++) { - const stepX = startX + (endX - startX) * (i / steps); - const stepY = startY + (endY - startY) * (i / steps); - await page.mouse.move(stepX, stepY); - await page.waitForTimeout(3); - } - - await page.waitForTimeout(100); - await page.mouse.up(); - await page.waitForTimeout(100); - } - } - - // Verify the element was moved successfully - await expect(sourceElement).toBeVisible(); - await page.getByRole('textbox', { name: 'New dashboard' }).click(); - await expect(page.getByRole('button', { name: 'Save' })).toBeVisible(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(55); - await page.waitForLoadState('networkidle'); -}); - -test('Can edit name and description of a dashboard', async ({ page }) => { - await page.goto('http://localhost:3000/app/dashboards/c0855f0f-f50a-424e-9e72-9e53711a7f6a'); - await expect(page.getByRole('textbox', { name: 'Add description...' })).toHaveValue(''); - - await page.getByRole('textbox', { name: 'New dashboard' }).click(); - await page.getByRole('textbox', { name: 'New dashboard' }).fill('Important Metrics NATE RULES'); - await page.getByRole('textbox', { name: 'Add description...' }).click(); - await page.getByRole('textbox', { name: 'Add description...' }).fill('HUH?'); - await expect(page.getByRole('textbox', { name: 'New dashboard' })).toBeVisible(); - await expect(page.getByRole('textbox', { name: 'New dashboard' })).toHaveValue( - 'Important Metrics NATE RULES' - ); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(100); - await page.waitForLoadState('networkidle'); - - await expect(page.getByRole('textbox', { name: 'New dashboard' })).toHaveValue( - 'Important Metrics NATE RULES' - ); - await page.getByRole('textbox', { name: 'New dashboard' }).click(); - await page.getByRole('textbox', { name: 'New dashboard' }).fill('Important Metrics'); - await page.getByRole('textbox', { name: 'Add description...' }).click(); - await expect(page.getByRole('textbox', { name: 'Add description...' })).toHaveValue('HUH?'); - - await expect(page.getByRole('textbox', { name: 'New dashboard' })).toHaveValue( - 'Important Metrics' - ); - await page.getByRole('textbox', { name: 'Add description...' }).fill(''); - await expect(page.getByRole('textbox', { name: 'Add description...' })).toBeEmpty(); - await page.getByRole('textbox', { name: 'New dashboard' }).click(); - await page.getByRole('textbox', { name: 'New dashboard' }).fill('Important Metrics SWAG'); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(400); - await page.waitForLoadState('networkidle'); - await page.getByTestId('segmented-trigger-file').click(); - await page.getByTestId('segmented-trigger-file').click(); - await page.waitForTimeout(1000); - await page.waitForLoadState('networkidle'); - await page.waitForLoadState('domcontentloaded'); - - await expect(page.getByRole('code').getByText('Important Metrics SWAG')).toBeVisible({ - timeout: 22000 +test.describe.serial('dashboard updates', () => { + test('Go to dashboard', async ({ page }) => { + await page.goto('http://localhost:3000/app/dashboards'); + await expect(page.getByText('Dashboards').nth(1)).toBeVisible(); + await expect(page.getByRole('button', { name: 'New dashboard' })).toBeVisible(); + await expect(page.getByRole('button', { name: '12px star' })).toBeVisible(); + await page.getByRole('link', { name: 'Important Metrics 12px star' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); + await expect(page.getByRole('textbox', { name: 'New dashboard' })).toHaveValue( + 'Important Metrics' + ); + }); + + test('Can remove a metric from a dashboard', async ({ page }) => { + await page.goto('http://localhost:3000/app/dashboards/c0855f0f-f50a-424e-9e72-9e53711a7f6a'); + await expect(page.getByRole('button', { name: 'Quarterly Gross Profit Margin' })).toBeVisible(); + // Hover over the metric to reveal the three-dot menu + await page + .locator(`[data-testid="metric-item-72e445a5-fb08-5b76-8c77-1642adf0cb72"]`) + .hover({ timeout: 500 }); + await expect( + page.locator(`[data-testid="metric-item-72e445a5-fb08-5b76-8c77-1642adf0cb72"]`) + ).toBeVisible(); + + await page + .getByTestId('metric-item-72e445a5-fb08-5b76-8c77-1642adf0cb72') + .locator('button') + .click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible(); + await page.getByRole('button', { name: 'Submit' }).click(); + await expect( + page.getByRole('button', { name: 'Quarterly Gross Profit Margin' }) + ).not.toBeVisible(); + await page + .locator('div') + .filter({ hasText: /^DashboardFileStart chat$/ }) + .getByRole('button') + .nth(2) + .click(); + await page.getByRole('textbox', { name: 'Search...' }).click(); + await page + .getByRole('textbox', { name: 'Search...' }) + .fill('Quarterly Gross Profit Margin Trend (Q2 2023 - Q1 2024)'); + await page.waitForTimeout(450); + await page.waitForLoadState('networkidle'); + await page + .locator('div:nth-child(2) > div > div > div > .border-border > div > .peer') + .first() + .click(); + await expect(page.getByRole('button', { name: 'Add metrics' })).toBeVisible(); + await page.getByRole('button', { name: 'Add metrics' }).click(); + await page.waitForTimeout(300); + await page.waitForLoadState('networkidle'); + await expect( + page.getByTestId('metric-item-72e445a5-fb08-5b76-8c77-1642adf0cb72') + ).toBeVisible(); + + //drag back into place + // Use a more specific selector if the element is a link + const sourceElement = page.getByRole('button', { name: 'Quarterly Gross Profit Margin' }); + const targetElement = page.getByRole('button', { name: 'Revenue by Product Category (' }); + + expect(sourceElement).toBeVisible(); + expect(targetElement).toBeVisible(); + + try { + // Skip the initial click since it's a link and would navigate away + // Go straight to hover and mouse operations + await sourceElement.hover({ force: true }); + await page.waitForTimeout(200); + await page.mouse.down(); + await page.waitForTimeout(200); + + // Move to target element + await targetElement.hover({ force: true, position: { x: 5, y: 5 } }); + await page.waitForTimeout(400); + + await page.mouse.up(); + await page.waitForTimeout(1000); + } catch (e) { + const sourceBoundingBox = await sourceElement.boundingBox(); + const targetBoundingBox = await targetElement.boundingBox(); + + if (sourceBoundingBox && targetBoundingBox) { + const startX = sourceBoundingBox.x + sourceBoundingBox.width / 2; + const startY = sourceBoundingBox.y + sourceBoundingBox.height / 2; + const endX = targetBoundingBox.x + 10; + const endY = targetBoundingBox.y + targetBoundingBox.height / 2; + + // Skip the initial click + await page.mouse.move(startX, startY); + await page.waitForTimeout(300); + await page.mouse.down(); + await page.waitForTimeout(300); + + // Move to destination with a slower motion + const steps = 20; + for (let i = 0; i <= steps; i++) { + const stepX = startX + (endX - startX) * (i / steps); + const stepY = startY + (endY - startY) * (i / steps); + await page.mouse.move(stepX, stepY); + await page.waitForTimeout(3); + } + + await page.waitForTimeout(100); + await page.mouse.up(); + await page.waitForTimeout(100); + } + } + + // Verify the element was moved successfully + await expect(sourceElement).toBeVisible(); + await page.getByRole('textbox', { name: 'New dashboard' }).click(); + await expect(page.getByRole('button', { name: 'Save' })).toBeVisible(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(55); + await page.waitForLoadState('networkidle'); + }); + + test('Can edit name and description of a dashboard', async ({ page }) => { + await page.goto('http://localhost:3000/app/dashboards/c0855f0f-f50a-424e-9e72-9e53711a7f6a'); + await expect(page.getByRole('textbox', { name: 'Add description...' })).toHaveValue(''); + + await page.getByRole('textbox', { name: 'New dashboard' }).click(); + await page.getByRole('textbox', { name: 'New dashboard' }).fill('Important Metrics NATE RULES'); + await page.getByRole('textbox', { name: 'Add description...' }).click(); + await page.getByRole('textbox', { name: 'Add description...' }).fill('HUH?'); + await expect(page.getByRole('textbox', { name: 'New dashboard' })).toBeVisible(); + await expect(page.getByRole('textbox', { name: 'New dashboard' })).toHaveValue( + 'Important Metrics NATE RULES' + ); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + + await expect(page.getByRole('textbox', { name: 'New dashboard' })).toHaveValue( + 'Important Metrics NATE RULES' + ); + await page.getByRole('textbox', { name: 'New dashboard' }).click(); + await page.getByRole('textbox', { name: 'New dashboard' }).fill('Important Metrics'); + await page.getByRole('textbox', { name: 'Add description...' }).click(); + await expect(page.getByRole('textbox', { name: 'Add description...' })).toHaveValue('HUH?'); + + await expect(page.getByRole('textbox', { name: 'New dashboard' })).toHaveValue( + 'Important Metrics' + ); + await page.getByRole('textbox', { name: 'Add description...' }).fill(''); + await expect(page.getByRole('textbox', { name: 'Add description...' })).toBeEmpty(); + await page.getByRole('textbox', { name: 'New dashboard' }).click(); + await page.getByRole('textbox', { name: 'New dashboard' }).fill('Important Metrics SWAG'); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(400); + await page.waitForLoadState('networkidle'); + await page.getByTestId('segmented-trigger-file').click(); + await page.getByTestId('segmented-trigger-file').click(); + await page.waitForTimeout(1000); + await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); + + await expect(page.getByRole('code').getByText('Important Metrics SWAG')).toBeVisible({ + timeout: 22000 + }); + await expect(page.locator('.current-line').first()).toBeVisible(); + await page.getByTestId('segmented-trigger-dashboard').click(); + await page.getByRole('textbox', { name: 'New dashboard' }).click(); + await page.getByRole('textbox', { name: 'New dashboard' }).fill('Important Metrics'); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByRole('textbox', { name: 'New dashboard' })).toHaveValue( + 'Important Metrics' + ); }); - await expect(page.locator('.current-line').first()).toBeVisible(); - await page.getByTestId('segmented-trigger-dashboard').click(); - await page.getByRole('textbox', { name: 'New dashboard' }).click(); - await page.getByRole('textbox', { name: 'New dashboard' }).fill('Important Metrics'); - await page.getByRole('button', { name: 'Save' }).click(); - await expect(page.getByRole('textbox', { name: 'New dashboard' })).toHaveValue( - 'Important Metrics' - ); }); diff --git a/web/playwright-tests/invite-user.test.ts b/web/playwright-tests/invite-user.test.ts index 6877b4aec..bce4b5923 100644 --- a/web/playwright-tests/invite-user.test.ts +++ b/web/playwright-tests/invite-user.test.ts @@ -1,67 +1,69 @@ import { test, expect } from '@playwright/test'; -test('Invite User', async ({ page }) => { - await page.goto('http://localhost:3000/app/home'); - await page - .locator('div') - .filter({ hasText: /^Invite people$/ }) - .first() - .click(); - await page.getByRole('textbox', { name: 'buster@bluthbananas.com,' }).click(); - await page - .getByRole('textbox', { name: 'buster@bluthbananas.com,' }) - .fill('nate+integration-test@buser.so'); - await page.getByRole('button', { name: 'Send invites' }).click(); - await page.waitForTimeout(100); - await page.waitForLoadState('networkidle'); - await page.waitForLoadState('domcontentloaded'); - await expect(page.getByText('Invites sent').first()).toBeVisible({ timeout: 3000 }); +test.describe.serial('invite user', () => { + test('Invite User', async ({ page }) => { + await page.goto('http://localhost:3000/app/home'); + await page + .locator('div') + .filter({ hasText: /^Invite people$/ }) + .first() + .click(); + await page.getByRole('textbox', { name: 'buster@bluthbananas.com,' }).click(); + await page + .getByRole('textbox', { name: 'buster@bluthbananas.com,' }) + .fill('nate+integration-test@buser.so'); + await page.getByRole('button', { name: 'Send invites' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); + await expect(page.getByText('Invites sent').first()).toBeVisible({ timeout: 3000 }); - await page.getByRole('button').filter({ hasText: /^$/ }).first().click(); - await page.getByRole('link', { name: 'Users' }).click(); - await page.waitForTimeout(5000); - await page.waitForLoadState('networkidle'); - await page.waitForLoadState('domcontentloaded'); - await expect(page.getByRole('link', { name: 'nate+integration-test@buser.' })).toBeVisible({ - timeout: 20000 - }); - await expect(page.getByRole('main')).toMatchAriaSnapshot(` + await page.getByRole('button').filter({ hasText: /^$/ }).first().click(); + await page.getByRole('link', { name: 'Users' }).click(); + await page.waitForTimeout(5000); + await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); + await expect(page.getByRole('link', { name: 'nate+integration-test@buser.' })).toBeVisible({ + timeout: 20000 + }); + await expect(page.getByRole('main')).toMatchAriaSnapshot(` - img - text: nate+integration-test@buser.so Restricted Querier `); - await page.getByRole('link', { name: 'nate+integration-test@buser.' }).click(); - await page.waitForTimeout(100); - await page.waitForLoadState('networkidle'); - await page.waitForLoadState('domcontentloaded'); - await expect(page.getByText('nate+integration-test@buser.so')).toBeVisible(); -}); + await page.getByRole('link', { name: 'nate+integration-test@buser.' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); + await expect(page.getByText('nate+integration-test@buser.so')).toBeVisible(); + }); -test('Can change user role', async ({ page }) => { - await page.goto('http://localhost:3000/app/settings/users'); - await page.getByRole('link', { name: 'B blake blake@buster.so' }).click(); - await page.waitForTimeout(1000); - await page.waitForLoadState('networkidle'); - await page.waitForLoadState('domcontentloaded'); - await expect(page.getByText('blake@buster.so')).toBeVisible(); - await expect(page.getByRole('combobox')).toHaveText(/Querier/); - await page.getByRole('combobox').click(); - await page.getByRole('option', { name: 'Workspace Admin' }).click(); - await expect( - page.locator('.text-text-secondary > div:nth-child(2) > .text-text-secondary').first() - ).toBeVisible(); - await page.waitForTimeout(25); - await page.waitForLoadState('networkidle'); - await page.reload(); - await expect( - page.locator('.text-text-secondary > div:nth-child(2) > .text-text-secondary').first() - ).toBeVisible(); - await page.getByRole('combobox').click(); - await page.getByRole('option', { name: 'Querier', exact: true }).click(); - await expect( - page.locator('.text-text-secondary > div:nth-child(2) > .text-text-secondary').first() - ).toBeVisible(); - await page.waitForTimeout(15); - await page.waitForLoadState('networkidle'); - await page.waitForLoadState('domcontentloaded'); + test('Can change user role', async ({ page }) => { + await page.goto('http://localhost:3000/app/settings/users'); + await page.getByRole('link', { name: 'B blake blake@buster.so' }).click(); + await page.waitForTimeout(1000); + await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); + await expect(page.getByText('blake@buster.so')).toBeVisible(); + await expect(page.getByRole('combobox')).toHaveText(/Querier/); + await page.getByRole('combobox').click(); + await page.getByRole('option', { name: 'Workspace Admin' }).click(); + await expect( + page.locator('.text-text-secondary > div:nth-child(2) > .text-text-secondary').first() + ).toBeVisible(); + await page.waitForTimeout(25); + await page.waitForLoadState('networkidle'); + await page.reload(); + await expect( + page.locator('.text-text-secondary > div:nth-child(2) > .text-text-secondary').first() + ).toBeVisible(); + await page.getByRole('combobox').click(); + await page.getByRole('option', { name: 'Querier', exact: true }).click(); + await expect( + page.locator('.text-text-secondary > div:nth-child(2) > .text-text-secondary').first() + ).toBeVisible(); + await page.waitForTimeout(15); + await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); + }); }); diff --git a/web/playwright-tests/line-chart-axis-tests.spec.ts b/web/playwright-tests/line-chart-axis-tests.spec.ts index a1bf998c7..e23960f59 100644 --- a/web/playwright-tests/line-chart-axis-tests.spec.ts +++ b/web/playwright-tests/line-chart-axis-tests.spec.ts @@ -1,320 +1,328 @@ import { test, expect } from '@playwright/test'; -test.skip('Line chart - x axis rotation', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' - ); - await page - .locator('div') - .filter({ hasText: /^X-Axis$/ }) - .getByRole('button') - .click(); - await page.getByTestId('segmented-trigger-45').click(); - expect(page.getByTestId('segmented-trigger-45')).toHaveAttribute('data-state', 'active'); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); - await page.waitForTimeout(50); - await page - .locator('div') - .filter({ hasText: /^X-Axis$/ }) - .getByRole('button') - .click(); - expect(page.getByTestId('segmented-trigger-45')).toHaveAttribute('data-state', 'active'); - await page.getByTestId('segmented-trigger-auto').click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); -}); +test.describe.serial('Line chart - axis tests', () => { + test.skip('Line chart - x axis rotation', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' + ); + await page + .locator('div') + .filter({ hasText: /^X-Axis$/ }) + .getByRole('button') + .click(); + await page.getByTestId('segmented-trigger-45').click(); + expect(page.getByTestId('segmented-trigger-45')).toHaveAttribute('data-state', 'active'); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(50); + await page + .locator('div') + .filter({ hasText: /^X-Axis$/ }) + .getByRole('button') + .click(); + expect(page.getByTestId('segmented-trigger-45')).toHaveAttribute('data-state', 'active'); + await page.getByTestId('segmented-trigger-auto').click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + }); -test('Line chart - line title', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' - ); - await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); - await page.getByRole('textbox', { name: 'Avg Revenue Per Customer' }).click(); - await page.getByRole('textbox', { name: 'Avg Revenue Per Customer' }).fill('NATE RULEZ'); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); - await expect(page.locator('body')).toMatchAriaSnapshot(` + test('Line chart - line title', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' + ); + await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); + await page.getByRole('textbox', { name: 'Avg Revenue Per Customer' }).click(); + await page.getByRole('textbox', { name: 'Avg Revenue Per Customer' }).fill('NATE RULEZ'); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + await expect(page.locator('body')).toMatchAriaSnapshot(` - textbox "New chart": /Average Revenue per Customer \\(Quarterly\\) \\(Q2 \\d+ - Q1 \\d+\\)/ - text: /Q2 \\d+ - Q1 \\d+ • What is the average revenue generated per customer quarterly from Q2 \\d+ to Q1 \\d+\\?/ - img `); - await expect(page.getByTestId('select-axis-drop-zone-yAxis')).toContainText('NATE RULEZ'); - await page.getByRole('textbox', { name: 'NATE RULEZ' }).click(); - await page.getByRole('textbox', { name: 'NATE RULEZ' }).press('ControlOrMeta+a'); - await page.getByRole('textbox', { name: 'NATE RULEZ' }).fill(''); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); - await expect(page.getByTestId('select-axis-drop-zone-yAxis')).not.toContainText('NATE RULEZ'); -}); + await expect(page.getByTestId('select-axis-drop-zone-yAxis')).toContainText('NATE RULEZ'); + await page.getByRole('textbox', { name: 'NATE RULEZ' }).click(); + await page.getByRole('textbox', { name: 'NATE RULEZ' }).press('ControlOrMeta+a'); + await page.getByRole('textbox', { name: 'NATE RULEZ' }).fill(''); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + await expect(page.getByTestId('select-axis-drop-zone-yAxis')).not.toContainText('NATE RULEZ'); + }); -test('Line chart - line settings', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' - ); - await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); - await page.getByTestId('segmented-trigger-dot-line').click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(20); - await page.waitForLoadState('networkidle'); - await expect(page.getByTestId('segmented-trigger-dot-line')).toHaveAttribute( - 'data-state', - 'active' - ); - await expect(page.locator('body')).toMatchAriaSnapshot(` + test('Line chart - line settings', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' + ); + await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); + await page.getByTestId('segmented-trigger-dot-line').click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(120); + await page.waitForLoadState('networkidle'); + await expect(page.getByTestId('segmented-trigger-dot-line')).toHaveAttribute( + 'data-state', + 'active' + ); + await expect(page.locator('body')).toMatchAriaSnapshot(` - textbox "New chart": /Average Revenue per Customer \\(Quarterly\\) \\(Q2 \\d+ - Q1 \\d+\\)/ - text: /Q2 \\d+ - Q1 \\d+ • What is the average revenue generated per customer quarterly from Q2 \\d+ to Q1 \\d+\\?/ - img `); - await page.getByTestId('segmented-trigger-step').click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(20); - await page.waitForLoadState('networkidle'); - await expect(page.getByTestId('segmented-trigger-step')).toHaveAttribute('data-state', 'active'); - await page.waitForTimeout(100); - await page.getByTestId('segmented-trigger-line').click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); - await expect(page.getByTestId('segmented-trigger-line')).toHaveAttribute('data-state', 'active'); -}); + await page.getByTestId('segmented-trigger-step').click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(120); + await page.waitForLoadState('networkidle'); + await expect(page.getByTestId('segmented-trigger-step')).toHaveAttribute( + 'data-state', + 'active' + ); + await page.waitForTimeout(100); + await page.getByTestId('segmented-trigger-line').click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + await expect(page.getByTestId('segmented-trigger-line')).toHaveAttribute( + 'data-state', + 'active' + ); + }); -test('Line chart - data labels', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' - ); - await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); - await page.getByRole('switch').click(); - await expect(page.getByRole('switch')).toBeVisible(); - await expect(page.getByRole('switch')).toHaveAttribute('data-state', 'checked'); + test('Line chart - data labels', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' + ); + await page.getByTestId('select-axis-drop-zone-yAxis').getByRole('button').nth(3).click(); + await page.getByRole('switch').click(); + await expect(page.getByRole('switch')).toBeVisible(); + await expect(page.getByRole('switch')).toHaveAttribute('data-state', 'checked'); - await page.getByRole('button', { name: 'Reset' }).click(); - await expect(page.getByRole('switch')).not.toHaveAttribute('data-state', 'checked'); -}); + await page.getByRole('button', { name: 'Reset' }).click(); + await expect(page.getByRole('switch')).not.toHaveAttribute('data-state', 'checked'); + }); -test('Line chart - styling updates - data labels', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' - ); - await page.getByTestId('segmented-trigger-Styling').click(); - await page - .locator('div') - .filter({ hasText: /^Data labels$/ }) - .getByRole('switch') - .click(); - await expect( - page + test('Line chart - styling updates - data labels', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' + ); + await page.getByTestId('segmented-trigger-Styling').click(); + await page .locator('div') .filter({ hasText: /^Data labels$/ }) .getByRole('switch') - ).toHaveAttribute('data-state', 'checked'); - await page.waitForTimeout(150); - await page.getByRole('button', { name: 'Reset' }).click(); - await page.waitForTimeout(150); - await expect( - page - .locator('div') - .filter({ hasText: /^Data labels$/ }) - .getByRole('switch') - ).toHaveAttribute('data-state', 'unchecked'); -}); + .click(); + await expect( + page + .locator('div') + .filter({ hasText: /^Data labels$/ }) + .getByRole('switch') + ).toHaveAttribute('data-state', 'checked'); + await page.waitForTimeout(150); + await page.getByRole('button', { name: 'Reset' }).click(); + await page.waitForTimeout(150); + await expect( + page + .locator('div') + .filter({ hasText: /^Data labels$/ }) + .getByRole('switch') + ).toHaveAttribute('data-state', 'unchecked'); + }); -test('Line chart - styling updates - grid lines', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' - ); - await page.getByTestId('segmented-trigger-Styling').click(); - await page - .locator('div') - .filter({ hasText: /^Grid lines$/ }) - .getByRole('switch') - .click(); - await expect( - page + test('Line chart - styling updates - grid lines', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' + ); + await page.getByTestId('segmented-trigger-Styling').click(); + await page .locator('div') .filter({ hasText: /^Grid lines$/ }) .getByRole('switch') - ).toHaveAttribute('data-state', 'unchecked'); - await page.getByRole('button', { name: 'Reset' }).click(); - await expect( - page - .locator('div') - .filter({ hasText: /^Grid lines$/ }) - .getByRole('switch') - ).toHaveAttribute('data-state', 'checked'); -}); + .click(); + await expect( + page + .locator('div') + .filter({ hasText: /^Grid lines$/ }) + .getByRole('switch') + ).toHaveAttribute('data-state', 'unchecked'); + await page.getByRole('button', { name: 'Reset' }).click(); + await expect( + page + .locator('div') + .filter({ hasText: /^Grid lines$/ }) + .getByRole('switch') + ).toHaveAttribute('data-state', 'checked'); + }); -test('Line chart - styling updates - hide y-axis', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' - ); - await page.getByTestId('segmented-trigger-Styling').click(); - await page - .locator('div') - .filter({ hasText: /^Hide y-axis$/ }) - .getByRole('switch') - .click(); - await expect( - page + test('Line chart - styling updates - hide y-axis', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' + ); + await page.getByTestId('segmented-trigger-Styling').click(); + await page .locator('div') .filter({ hasText: /^Hide y-axis$/ }) .getByRole('switch') - ).toHaveAttribute('data-state', 'checked'); - await page.getByRole('button', { name: 'Reset' }).click(); - await expect( - page - .locator('div') - .filter({ hasText: /^Hide y-axis$/ }) - .getByRole('switch') - ).toHaveAttribute('data-state', 'unchecked'); -}); + .click(); + await expect( + page + .locator('div') + .filter({ hasText: /^Hide y-axis$/ }) + .getByRole('switch') + ).toHaveAttribute('data-state', 'checked'); + await page.getByRole('button', { name: 'Reset' }).click(); + await expect( + page + .locator('div') + .filter({ hasText: /^Hide y-axis$/ }) + .getByRole('switch') + ).toHaveAttribute('data-state', 'unchecked'); + }); -test('Line chart - styling updates - smooth lines', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' - ); - await page.getByTestId('segmented-trigger-Styling').click(); - await page - .locator('div') - .filter({ hasText: /^Smooth lines$/ }) - .getByRole('switch') - .click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); - - await page.reload(); - await page.getByTestId('segmented-trigger-Styling').click(); - - await expect( - page + test('Line chart - styling updates - smooth lines', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' + ); + await page.getByTestId('segmented-trigger-Styling').click(); + await page .locator('div') .filter({ hasText: /^Smooth lines$/ }) .getByRole('switch') - ).toHaveAttribute('data-state', 'checked'); - await page - .locator('div') - .filter({ hasText: /^Smooth lines$/ }) - .getByRole('switch') - .click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); - await page.reload(); - await page.getByTestId('segmented-trigger-Styling').click(); - await expect( - page + .click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + + await page.reload(); + await page.getByTestId('segmented-trigger-Styling').click(); + + await expect( + page + .locator('div') + .filter({ hasText: /^Smooth lines$/ }) + .getByRole('switch') + ).toHaveAttribute('data-state', 'checked'); + await page .locator('div') .filter({ hasText: /^Smooth lines$/ }) .getByRole('switch') - ).toHaveAttribute('data-state', 'unchecked'); -}); + .click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + await page.reload(); + await page.getByTestId('segmented-trigger-Styling').click(); + await expect( + page + .locator('div') + .filter({ hasText: /^Smooth lines$/ }) + .getByRole('switch') + ).toHaveAttribute('data-state', 'unchecked'); + }); -test('Line chart - styling updates - dots on lines', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' - ); - await page.getByTestId('segmented-trigger-Styling').click(); - await page - .locator('div') - .filter({ hasText: /^Dot on lines$/ }) - .getByRole('switch') - .click(); - await page.getByRole('button', { name: 'Save' }).click(); - - await page.reload(); - await page.getByTestId('segmented-trigger-Styling').click(); - await expect( - page + test('Line chart - styling updates - dots on lines', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' + ); + await page.getByTestId('segmented-trigger-Styling').click(); + await page .locator('div') .filter({ hasText: /^Dot on lines$/ }) .getByRole('switch') - ).toHaveAttribute('data-state', 'checked'); + .click(); + await page.getByRole('button', { name: 'Save' }).click(); - await page.reload(); - await page.getByTestId('segmented-trigger-Styling').click(); - await page - .locator('div') - .filter({ hasText: /^Dot on lines$/ }) - .getByRole('switch') - .click(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); - await expect( - page + await page.reload(); + await page.getByTestId('segmented-trigger-Styling').click(); + await expect( + page + .locator('div') + .filter({ hasText: /^Dot on lines$/ }) + .getByRole('switch') + ).toHaveAttribute('data-state', 'checked'); + + await page.reload(); + await page.getByTestId('segmented-trigger-Styling').click(); + await page .locator('div') .filter({ hasText: /^Dot on lines$/ }) .getByRole('switch') - ).toHaveAttribute('data-state', 'unchecked'); -}); + .click(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + await expect( + page + .locator('div') + .filter({ hasText: /^Dot on lines$/ }) + .getByRole('switch') + ).toHaveAttribute('data-state', 'unchecked'); + }); -test('Line chart - when legend headline is turned it it also turns on show legend', async ({ - page -}) => { - await page.goto( - 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' - ); - await page.getByTestId('segmented-trigger-Styling').click(); - await expect( + test('Line chart - when legend headline is turned it it also turns on show legend', async ({ page - .locator('div') - .filter({ hasText: /^Show legend$/ }) - .getByRole('switch') - ).toHaveAttribute('data-state', 'unchecked'); - await page.getByRole('combobox').filter({ hasText: 'None' }).click(); - await page.getByRole('option', { name: 'Current' }).click(); - await expect( - page - .locator('div') - .filter({ hasText: /^Show legend$/ }) - .getByRole('switch') - ).toHaveAttribute('data-state', 'checked'); + }) => { + await page.goto( + 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' + ); + await page.getByTestId('segmented-trigger-Styling').click(); + await expect( + page + .locator('div') + .filter({ hasText: /^Show legend$/ }) + .getByRole('switch') + ).toHaveAttribute('data-state', 'unchecked'); + await page.getByRole('combobox').filter({ hasText: 'None' }).click(); + await page.getByRole('option', { name: 'Current' }).click(); + await expect( + page + .locator('div') + .filter({ hasText: /^Show legend$/ }) + .getByRole('switch') + ).toHaveAttribute('data-state', 'checked'); - await page.getByRole('combobox').filter({ hasText: 'Current' }).click(); - await page.getByRole('option', { name: 'None' }).click(); - await page.getByRole('button', { name: 'Reset' }).click(); - await expect( - page - .locator('div') - .filter({ hasText: /^Show legend$/ }) - .getByRole('switch') - ).toHaveAttribute('data-state', 'unchecked'); -}); + await page.getByRole('combobox').filter({ hasText: 'Current' }).click(); + await page.getByRole('option', { name: 'None' }).click(); + await page.getByRole('button', { name: 'Reset' }).click(); + await expect( + page + .locator('div') + .filter({ hasText: /^Show legend$/ }) + .getByRole('switch') + ).toHaveAttribute('data-state', 'unchecked'); + }); -test('Line chart - can reset colors', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' - ); - await page.getByTestId('segmented-trigger-Styling').click(); - await page.getByTestId('segmented-trigger-Colors').click(); - await page.getByTestId('segmented-trigger-Monochrome').click(); - await page.locator('div').filter({ hasText: /^Red$/ }).first().click(); - await expect(page.getByRole('button', { name: 'Reset' })).toBeVisible(); - await page.getByRole('button', { name: 'Reset' }).click(); - await expect(page.getByRole('button', { name: 'Reset' })).not.toBeVisible(); -}); + test('Line chart - can reset colors', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' + ); + await page.getByTestId('segmented-trigger-Styling').click(); + await page.getByTestId('segmented-trigger-Colors').click(); + await page.getByTestId('segmented-trigger-Monochrome').click(); + await page.locator('div').filter({ hasText: /^Red$/ }).first().click(); + await expect(page.getByRole('button', { name: 'Reset' })).toBeVisible(); + await page.getByRole('button', { name: 'Reset' }).click(); + await expect(page.getByRole('button', { name: 'Reset' })).not.toBeVisible(); + }); -test('Line chart - when trying to navigate away it will warn you', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' - ); - await page.getByTestId('segmented-trigger-Styling').click(); - await page.getByTestId('segmented-trigger-Colors').click(); - await page.getByTestId('segmented-trigger-Monochrome').click(); - await page.locator('div').filter({ hasText: /^Red$/ }).first().click(); - await page.getByTestId('segmented-trigger-results').click(); - await expect(page.getByRole('button', { name: 'Discard changes' })).toBeVisible(); - await page.getByRole('button').filter({ hasText: /^$/ }).click(); - await expect(page.getByRole('button', { name: 'Discard changes' })).not.toBeVisible(); - await page.getByRole('button', { name: 'Reset' }).click(); - await page.getByTestId('segmented-trigger-results').click(); - const expectedURL = - 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/results'; - await expect(page.getByRole('button', { name: 'Discard changes' })).not.toBeVisible(); - await expect(page).toHaveURL(expectedURL, { timeout: 15000 }); + test('Line chart - when trying to navigate away it will warn you', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/chart?secondary_view=chart-edit' + ); + await page.getByTestId('segmented-trigger-Styling').click(); + await page.getByTestId('segmented-trigger-Colors').click(); + await page.getByTestId('segmented-trigger-Monochrome').click(); + await page.locator('div').filter({ hasText: /^Red$/ }).first().click(); + await page.getByTestId('segmented-trigger-results').click(); + await expect(page.getByRole('button', { name: 'Discard changes' })).toBeVisible(); + await page.getByRole('button').filter({ hasText: /^$/ }).click(); + await expect(page.getByRole('button', { name: 'Discard changes' })).not.toBeVisible(); + await page.getByRole('button', { name: 'Reset' }).click(); + await page.getByTestId('segmented-trigger-results').click(); + const expectedURL = + 'http://localhost:3000/app/metrics/635d9b06-afb1-5b05-8130-03c0b7a04bcb/results'; + await expect(page.getByRole('button', { name: 'Discard changes' })).not.toBeVisible(); + await expect(page).toHaveURL(expectedURL, { timeout: 15000 }); + }); }); diff --git a/web/playwright-tests/metric-chart-updates.spec.ts b/web/playwright-tests/metric-chart-updates.spec.ts index a189e4398..eababaa28 100644 --- a/web/playwright-tests/metric-chart-updates.spec.ts +++ b/web/playwright-tests/metric-chart-updates.spec.ts @@ -1,72 +1,74 @@ import { test, expect } from '@playwright/test'; -test('Metric can change to be a table', async ({ page }) => { - await page.goto('http://localhost:3000/app/home'); - await page.getByRole('link', { name: 'Metrics', exact: true }).click(); - await page.getByRole('link', { name: 'Total Unique Products Sold' }).click(); +test.describe.serial('Metric chart updates', () => { + test('Metric can change to be a table', async ({ page }) => { + await page.goto('http://localhost:3000/app/home'); + await page.getByRole('link', { name: 'Metrics', exact: true }).click(); + await page.getByRole('link', { name: 'Total Unique Products Sold' }).click(); - await page.getByTestId('edit-chart-button').getByRole('button').click(); - expect(page.getByTestId('metric-view-chart-content')).toBeVisible(); + await page.getByTestId('edit-chart-button').getByRole('button').click(); + expect(page.getByTestId('metric-view-chart-content')).toBeVisible(); - expect(page.getByTestId('select-chart-type-column')).toBeVisible(); - expect(page.getByTestId('select-chart-type-column')).toBeDisabled(); - expect(page.getByTestId('select-chart-type-table')).not.toBeDisabled(); + expect(page.getByTestId('select-chart-type-column')).toBeVisible(); + expect(page.getByTestId('select-chart-type-column')).toBeDisabled(); + expect(page.getByTestId('select-chart-type-table')).not.toBeDisabled(); - // - await page.getByTestId('select-chart-type-table').click(); - await expect( - page - .locator('div') - .filter({ hasText: /^Unsaved changesResetSave$/ }) - .nth(1) - ).toBeVisible(); - await page.getByTestId('select-chart-type-metric').click(); - await expect( - page - .locator('div') - .filter({ hasText: /^Unsaved changesResetSave$/ }) - .nth(1) - ).toBeHidden(); -}); - -test('Metric can metric headers', async ({ page }) => { - await page.goto('http://localhost:3000/app/home'); - await page.getByRole('link', { name: 'Metrics', exact: true }).click(); - await page.getByRole('link', { name: 'Total Unique Products Sold' }).click(); - await page.getByTestId('edit-chart-button').getByRole('button').click(); - // - - await page.getByTestId('edit-metric-header-type').click(); - await page.getByRole('option', { name: 'Custom' }).click(); - await page.getByRole('textbox', { name: 'Enter header' }).fill('WOW!'); - await expect(page.getByRole('heading', { name: 'WOW!' })).toBeVisible(); - await page.getByRole('textbox', { name: 'Enter header' }).click(); - await page.getByRole('textbox', { name: 'Enter header' }).fill(''); - await expect(page.getByRole('heading', { name: 'WOW!' })).toBeHidden(); - - //Header options - await page.getByTestId('edit-metric-header-type').click(); - await page.getByRole('option', { name: 'Column title' }).click(); - await expect(page.getByRole('heading', { name: 'Total Unique Products Sold' })).toBeVisible(); - await page.getByTestId('edit-metric-header-type').click(); - await page.getByRole('option', { name: 'Column value' }).click(); - await expect(page.locator('h4')).toBeVisible(); - await page.getByRole('button', { name: 'Reset' }).click(); - - //Subheader options - - await page.getByTestId('edit-metric-subheader-type').click(); - await page.getByRole('option', { name: 'Custom' }).click(); - await page.getByRole('textbox', { name: 'Enter sub-header' }).fill('Cool!'); - await expect(page.getByRole('heading', { name: 'Cool!' })).toBeVisible(); - await page.getByTestId('edit-metric-subheader-type').click(); - await page.getByText('Column title').click(); - await expect(page.getByRole('heading', { name: 'Total Unique Products Sold' })).toBeVisible(); - await page.getByTestId('edit-metric-subheader-type').click(); - await page.getByRole('option', { name: 'Column value' }).click(); - - await page.waitForTimeout(1000); - await expect(page.locator('h4')).toBeVisible(); - await page.getByRole('button', { name: 'Reset' }).click(); - await expect(page.locator('h4')).toBeHidden(); + // + await page.getByTestId('select-chart-type-table').click(); + await expect( + page + .locator('div') + .filter({ hasText: /^Unsaved changesResetSave$/ }) + .nth(1) + ).toBeVisible(); + await page.getByTestId('select-chart-type-metric').click(); + await expect( + page + .locator('div') + .filter({ hasText: /^Unsaved changesResetSave$/ }) + .nth(1) + ).toBeHidden(); + }); + + test('Metric can metric headers', async ({ page }) => { + await page.goto('http://localhost:3000/app/home'); + await page.getByRole('link', { name: 'Metrics', exact: true }).click(); + await page.getByRole('link', { name: 'Total Unique Products Sold' }).click(); + await page.getByTestId('edit-chart-button').getByRole('button').click(); + // + + await page.getByTestId('edit-metric-header-type').click(); + await page.getByRole('option', { name: 'Custom' }).click(); + await page.getByRole('textbox', { name: 'Enter header' }).fill('WOW!'); + await expect(page.getByRole('heading', { name: 'WOW!' })).toBeVisible(); + await page.getByRole('textbox', { name: 'Enter header' }).click(); + await page.getByRole('textbox', { name: 'Enter header' }).fill(''); + await expect(page.getByRole('heading', { name: 'WOW!' })).toBeHidden(); + + //Header options + await page.getByTestId('edit-metric-header-type').click(); + await page.getByRole('option', { name: 'Column title' }).click(); + await expect(page.getByRole('heading', { name: 'Total Unique Products Sold' })).toBeVisible(); + await page.getByTestId('edit-metric-header-type').click(); + await page.getByRole('option', { name: 'Column value' }).click(); + await expect(page.locator('h4')).toBeVisible(); + await page.getByRole('button', { name: 'Reset' }).click(); + + //Subheader options + + await page.getByTestId('edit-metric-subheader-type').click(); + await page.getByRole('option', { name: 'Custom' }).click(); + await page.getByRole('textbox', { name: 'Enter sub-header' }).fill('Cool!'); + await expect(page.getByRole('heading', { name: 'Cool!' })).toBeVisible(); + await page.getByTestId('edit-metric-subheader-type').click(); + await page.getByText('Column title').click(); + await expect(page.getByRole('heading', { name: 'Total Unique Products Sold' })).toBeVisible(); + await page.getByTestId('edit-metric-subheader-type').click(); + await page.getByRole('option', { name: 'Column value' }).click(); + + await page.waitForTimeout(1000); + await expect(page.locator('h4')).toBeVisible(); + await page.getByRole('button', { name: 'Reset' }).click(); + await expect(page.locator('h4')).toBeHidden(); + }); }); diff --git a/web/playwright-tests/sharing-metric.test.ts b/web/playwright-tests/sharing-metric.test.ts index d5fb5335a..9a5a59d6f 100644 --- a/web/playwright-tests/sharing-metric.test.ts +++ b/web/playwright-tests/sharing-metric.test.ts @@ -1,41 +1,43 @@ import { test, expect } from '@playwright/test'; -test('Can share a metric', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/chats/865352e8-c327-461d-ae67-9efeb530ff0e/metrics/1e91b291-8883-5451-8b98-89e99071e4f8/chart?metric_version_number=1' - ); - await page.getByTestId('share-button').click(); - await page.getByRole('textbox', { name: 'Invite others by email...' }).click(); - await page.getByRole('textbox', { name: 'Invite others by email...' }).fill('blake@buster.so'); - await page.getByRole('button', { name: 'Invite' }).click(); - await page.waitForTimeout(100); - await page.waitForLoadState('networkidle'); - await expect(page.getByText('Can view')).toBeVisible(); - await page.getByText('Can view').click(); - await page.getByRole('menuitemcheckbox', { name: 'Remove' }).click(); - await expect(page.getByText('Can view')).toBeHidden(); - await page.waitForTimeout(100); - await page.waitForLoadState('networkidle'); -}); - -test('Can publish a metric', async ({ page }) => { - await page.goto( - 'http://localhost:3000/app/chats/865352e8-c327-461d-ae67-9efeb530ff0e/metrics/1e91b291-8883-5451-8b98-89e99071e4f8/chart?metric_version_number=1' - ); - await page.getByTestId('share-button').click(); - await page.getByTestId('segmented-trigger-Publish').click(); - await expect(page.getByRole('button', { name: 'Create public link' })).toBeVisible(); - await page.getByRole('button', { name: 'Create public link' }).click(); - await expect(page.getByText('Live on the web')).toBeVisible(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); - - await page.reload(); - - await page.getByTestId('share-button').click(); - await page.getByTestId('segmented-trigger-Publish').click(); - await page.getByRole('button', { name: 'Unpublish' }).click(); - await page.waitForTimeout(50); - await page.waitForLoadState('networkidle'); - await expect(page.getByRole('button', { name: 'Create public link' })).toBeVisible(); +test.describe.serial('Sharing metric', () => { + test('Can share a metric', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/chats/865352e8-c327-461d-ae67-9efeb530ff0e/metrics/1e91b291-8883-5451-8b98-89e99071e4f8/chart?metric_version_number=1' + ); + await page.getByTestId('share-button').click(); + await page.getByRole('textbox', { name: 'Invite others by email...' }).click(); + await page.getByRole('textbox', { name: 'Invite others by email...' }).fill('blake@buster.so'); + await page.getByRole('button', { name: 'Invite' }).click(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + await expect(page.getByText('Can view')).toBeVisible(); + await page.getByText('Can view').click(); + await page.getByRole('menuitemcheckbox', { name: 'Remove' }).click(); + await expect(page.getByText('Can view')).toBeHidden(); + await page.waitForTimeout(100); + await page.waitForLoadState('networkidle'); + }); + + test('Can publish a metric', async ({ page }) => { + await page.goto( + 'http://localhost:3000/app/chats/865352e8-c327-461d-ae67-9efeb530ff0e/metrics/1e91b291-8883-5451-8b98-89e99071e4f8/chart?metric_version_number=1' + ); + await page.getByTestId('share-button').click(); + await page.getByTestId('segmented-trigger-Publish').click(); + await expect(page.getByRole('button', { name: 'Create public link' })).toBeVisible(); + await page.getByRole('button', { name: 'Create public link' }).click(); + await expect(page.getByText('Live on the web')).toBeVisible(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + + await page.reload(); + + await page.getByTestId('share-button').click(); + await page.getByTestId('segmented-trigger-Publish').click(); + await page.getByRole('button', { name: 'Unpublish' }).click(); + await page.waitForTimeout(50); + await page.waitForLoadState('networkidle'); + await expect(page.getByRole('button', { name: 'Create public link' })).toBeVisible(); + }); }); diff --git a/web/playwright.config.ts b/web/playwright.config.ts index f2c9d023d..5247d85d2 100644 --- a/web/playwright.config.ts +++ b/web/playwright.config.ts @@ -23,7 +23,7 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 1, /* Run 3 tests in parallel */ - workers: process.env.CI ? 1 : 1, + workers: process.env.CI ? 1 : 10, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ diff --git a/web/src/app/auth/layout.tsx b/web/src/app/auth/layout.tsx index a7025e07c..7e5df271b 100644 --- a/web/src/app/auth/layout.tsx +++ b/web/src/app/auth/layout.tsx @@ -14,7 +14,7 @@ const LoginLayout: React.FC> = async ({ children }) => {
-
{children}
+
{children}
diff --git a/web/src/components/features/sidebars/SidebarPrimary.tsx b/web/src/components/features/sidebars/SidebarPrimary.tsx index 530b7bc2d..8fde4e332 100644 --- a/web/src/components/features/sidebars/SidebarPrimary.tsx +++ b/web/src/components/features/sidebars/SidebarPrimary.tsx @@ -1,14 +1,13 @@ 'use client'; -import React, { useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { Sidebar } from '@/components/ui/sidebar/Sidebar'; import { BusterLogoWithText } from '@/assets/svg/BusterLogoWithText'; import { BusterRoutes, createBusterRoute } from '@/routes'; import type { ISidebarGroup, ISidebarList, SidebarProps } from '@/components/ui/sidebar'; -import { BookOpen4, Flag, Gear, House4, Table, UnorderedList2, Plus } from '@/components/ui/icons'; +import { Flag, Gear, House4, Table, UnorderedList2, Plus } from '@/components/ui/icons'; import { PencilSquareIcon } from '@/components/ui/icons/customIcons/Pencil_Square'; -import { ASSET_ICONS, assetTypeToIcon, assetTypeToRoute } from '../config/assetIcons'; -import type { BusterUserFavorite } from '@/api/asset_interfaces/users'; +import { ASSET_ICONS } from '../config/assetIcons'; import { Button } from '@/components/ui/buttons'; import { Tooltip } from '@/components/ui/tooltip/Tooltip'; import Link from 'next/link'; @@ -21,58 +20,75 @@ import { SupportModal } from '../modal/SupportModal'; import { InvitePeopleModal } from '../modal/InvitePeopleModal'; import { useMemoizedFn } from '@/hooks'; import { SidebarUserFooter } from './SidebarUserFooter/SidebarUserFooter'; -import { - useDeleteUserFavorite, - useGetUserFavorites, - useUpdateUserFavorites -} from '@/api/buster_rest'; import { useHotkeys } from 'react-hotkeys-hook'; import { useInviteModalStore } from '@/context/BusterAppLayout'; +import { useFavoriteSidebarPanel } from './useFavoritesSidebarPanel'; +import { ShareAssetType } from '@/api/asset_interfaces/share'; -const topItems: ISidebarList = { - id: 'top-items', - items: [ - { - label: 'Home', - icon: , - route: BusterRoutes.APP_HOME, - id: BusterRoutes.APP_HOME - }, - { - label: 'Chat history', - icon: , - route: BusterRoutes.APP_CHAT, - id: BusterRoutes.APP_CHAT - } - ] +const topItems = ( + currentParentRoute: BusterRoutes, + favoritedPageType: ShareAssetType | null +): ISidebarList => { + const isActiveCheck = (type: ShareAssetType, route: BusterRoutes) => currentParentRoute === route; + + return { + id: 'top-items', + items: [ + { + label: 'Home', + icon: , + route: BusterRoutes.APP_HOME, + id: BusterRoutes.APP_HOME, + active: currentParentRoute === BusterRoutes.APP_HOME + }, + { + label: 'Chat history', + icon: , + route: BusterRoutes.APP_CHAT, + id: BusterRoutes.APP_CHAT, + active: isActiveCheck(ShareAssetType.CHAT, BusterRoutes.APP_CHAT) + } + ] + }; }; -const yourStuff: ISidebarGroup = { - label: 'Your stuff', - id: 'your-stuff', - items: [ - { - label: 'Metrics', - icon: , - route: BusterRoutes.APP_METRIC, - id: BusterRoutes.APP_METRIC - }, - { - label: 'Dashboards', - icon: , - route: BusterRoutes.APP_DASHBOARDS, - id: BusterRoutes.APP_DASHBOARDS - }, - { - label: 'Collections', - icon: , - route: BusterRoutes.APP_COLLECTIONS, - id: BusterRoutes.APP_COLLECTIONS - } - ] +const yourStuff = ( + currentParentRoute: BusterRoutes, + favoritedPageType: ShareAssetType | null +): ISidebarGroup => { + const isActiveCheck = (type: ShareAssetType, route: BusterRoutes) => + favoritedPageType !== type && favoritedPageType === null && currentParentRoute === route; + + return { + label: 'Your stuff', + id: 'your-stuff', + items: [ + { + label: 'Metrics', + icon: , + route: BusterRoutes.APP_METRIC, + id: BusterRoutes.APP_METRIC, + active: isActiveCheck(ShareAssetType.METRIC, BusterRoutes.APP_METRIC) + }, + { + label: 'Dashboards', + icon: , + route: BusterRoutes.APP_DASHBOARDS, + id: BusterRoutes.APP_DASHBOARDS, + active: isActiveCheck(ShareAssetType.DASHBOARD, BusterRoutes.APP_DASHBOARDS) + }, + { + label: 'Collections', + icon: , + route: BusterRoutes.APP_COLLECTIONS, + id: BusterRoutes.APP_COLLECTIONS, + active: isActiveCheck(ShareAssetType.COLLECTION, BusterRoutes.APP_COLLECTIONS) + } + ] + }; }; -const adminTools: ISidebarGroup = { +const adminTools = (currentParentRoute: BusterRoutes): ISidebarGroup => ({ label: 'Admin tools', id: 'admin-tools', items: [ @@ -94,8 +110,11 @@ const adminTools: ISidebarGroup = { route: BusterRoutes.APP_DATASETS, id: BusterRoutes.APP_DATASETS } - ] -}; + ].map((x) => ({ + ...x, + active: x.route === currentParentRoute + })) +}); const tryGroup = ( onClickInvitePeople: () => void, @@ -126,36 +145,46 @@ const tryGroup = ( export const SidebarPrimary = React.memo(() => { const isAdmin = useUserConfigContextSelector((x) => x.isAdmin); const isUserRegistered = useUserConfigContextSelector((x) => x.isUserRegistered); - const { data: favorites } = useGetUserFavorites(); const currentParentRoute = useAppLayoutContextSelector((x) => x.currentParentRoute); const onToggleInviteModal = useInviteModalStore((s) => s.onToggleInviteModal); const onOpenContactSupportModal = useContactSupportModalStore((s) => s.onOpenContactSupportModal); - const { mutateAsync: updateUserFavorites } = useUpdateUserFavorites(); - const { mutateAsync: deleteUserFavorite } = useDeleteUserFavorite(); - const onFavoritesReorder = useMemoizedFn((itemIds: string[]) => { - updateUserFavorites(itemIds); - }); + const { favoritesDropdownItems, favoritedPageType } = useFavoriteSidebarPanel(); + + const topItemsItems = useMemo( + () => topItems(currentParentRoute, favoritedPageType), + [currentParentRoute, favoritedPageType] + ); + + const adminToolsItems = useMemo(() => { + if (!isAdmin) return null; + return adminTools(currentParentRoute); + }, [isAdmin, currentParentRoute]); + + const yourStuffItems = useMemo( + () => yourStuff(currentParentRoute, favoritedPageType), + [currentParentRoute, favoritedPageType] + ); const sidebarItems: SidebarProps['content'] = useMemo(() => { if (!isUserRegistered) return []; - const items = [topItems]; + const items = [topItemsItems]; - if (isAdmin) { - items.push(adminTools); + if (adminToolsItems) { + items.push(adminToolsItems); } - items.push(yourStuff); + items.push(yourStuffItems); - if (favorites && favorites.length > 0) { - items.push(favoritesDropdown(favorites, { deleteUserFavorite, onFavoritesReorder })); + if (favoritesDropdownItems) { + items.push(favoritesDropdownItems); } items.push(tryGroup(onToggleInviteModal, () => onOpenContactSupportModal('feedback'), isAdmin)); return items; - }, [isAdmin, isUserRegistered, favorites, currentParentRoute, onFavoritesReorder]); + }, [isUserRegistered, adminToolsItems, yourStuffItems, favoritesDropdownItems]); const onCloseSupportModal = useMemoizedFn(() => onOpenContactSupportModal(false)); @@ -167,12 +196,7 @@ export const SidebarPrimary = React.memo(() => { return ( <> - + @@ -233,32 +257,3 @@ const GlobalModals = ({ onCloseSupportModal }: { onCloseSupportModal: () => void ); }; GlobalModals.displayName = 'GlobalModals'; - -const favoritesDropdown = ( - favorites: BusterUserFavorite[], - { - onFavoritesReorder, - deleteUserFavorite - }: { - onFavoritesReorder: (itemIds: string[]) => void; - deleteUserFavorite: (itemIds: string[]) => void; - } -): ISidebarGroup => { - return { - label: 'Favorites', - id: 'favorites', - isSortable: true, - onItemsReorder: onFavoritesReorder, - items: favorites.map((favorite) => { - const Icon = assetTypeToIcon(favorite.asset_type); - const route = assetTypeToRoute(favorite.asset_type, favorite.id); - return { - label: favorite.name, - icon: , - route, - id: favorite.id, - onRemove: () => deleteUserFavorite([favorite.id]) - }; - }) - }; -}; diff --git a/web/src/components/features/sidebars/SidebarSettings.tsx b/web/src/components/features/sidebars/SidebarSettings.tsx index 9a8cbf1a0..d3ea603c3 100644 --- a/web/src/components/features/sidebars/SidebarSettings.tsx +++ b/web/src/components/features/sidebars/SidebarSettings.tsx @@ -9,7 +9,7 @@ import { useUserConfigContextSelector } from '@/context/Users'; import { useAppLayoutContextSelector } from '@/context/BusterAppLayout'; import { SidebarUserFooter } from './SidebarUserFooter/SidebarUserFooter'; -const accountItems: ISidebarGroup = { +const accountItems = (currentParentRoute: BusterRoutes): ISidebarGroup => ({ label: 'Account', variant: 'icon', id: 'account', @@ -18,12 +18,13 @@ const accountItems: ISidebarGroup = { { label: 'Profile', route: createBusterRoute({ route: BusterRoutes.SETTINGS_PROFILE }), - id: createBusterRoute({ route: BusterRoutes.SETTINGS_PROFILE }) + id: BusterRoutes.SETTINGS_PROFILE, + active: currentParentRoute === BusterRoutes.SETTINGS_PROFILE } ] -}; +}); -const workspaceItems: ISidebarGroup = { +const workspaceItems = (currentParentRoute: BusterRoutes): ISidebarGroup => ({ label: 'Workspace', variant: 'icon', id: 'workspace', @@ -32,17 +33,20 @@ const workspaceItems: ISidebarGroup = { { label: 'API Keys', route: createBusterRoute({ route: BusterRoutes.SETTINGS_API_KEYS }), - id: createBusterRoute({ route: BusterRoutes.SETTINGS_API_KEYS }) + id: BusterRoutes.SETTINGS_API_KEYS }, { label: 'Data Sources', route: createBusterRoute({ route: BusterRoutes.SETTINGS_DATASOURCES }), - id: createBusterRoute({ route: BusterRoutes.SETTINGS_DATASOURCES }) + id: BusterRoutes.SETTINGS_DATASOURCES } - ] -}; + ].map((item) => ({ + ...item, + active: currentParentRoute === item.id + })) +}); -const permissionAndSecurityItems: ISidebarGroup = { +const permissionAndSecurityItems = (currentParentRoute: BusterRoutes): ISidebarGroup => ({ label: 'Permission & Security', variant: 'icon', id: 'permission-and-security', @@ -63,21 +67,24 @@ const permissionAndSecurityItems: ISidebarGroup = { route: createBusterRoute({ route: BusterRoutes.SETTINGS_PERMISSION_GROUPS }), id: createBusterRoute({ route: BusterRoutes.SETTINGS_PERMISSION_GROUPS }) } - ] -}; + ].map((item) => ({ + ...item, + active: currentParentRoute === item.id + })) +}); export const SidebarSettings: React.FC<{}> = React.memo(({}) => { const isAdmin = useUserConfigContextSelector((x) => x.isAdmin); const currentParentRoute = useAppLayoutContextSelector((x) => x.currentParentRoute); const content = useMemo(() => { - const items = [accountItems]; + const items = [accountItems(currentParentRoute)]; if (isAdmin) { - items.push(workspaceItems); - items.push(permissionAndSecurityItems); + items.push(workspaceItems(currentParentRoute)); + items.push(permissionAndSecurityItems(currentParentRoute)); } return items; - }, [isAdmin]); + }, [isAdmin, currentParentRoute]); return ( = React.memo(({}) => { ), [] )} - activeItem={currentParentRoute} footer={useMemo( () => ( diff --git a/web/src/components/features/sidebars/useFavoritesSidebarPanel.test.tsx b/web/src/components/features/sidebars/useFavoritesSidebarPanel.test.tsx new file mode 100644 index 000000000..6d98a557b --- /dev/null +++ b/web/src/components/features/sidebars/useFavoritesSidebarPanel.test.tsx @@ -0,0 +1,160 @@ +import { renderHook, act } from '@testing-library/react'; +import { useFavoriteSidebarPanel } from './useFavoritesSidebarPanel'; +import { + useGetUserFavorites, + useUpdateUserFavorites, + useDeleteUserFavorite +} from '@/api/buster_rest/users'; +import { useParams } from 'next/navigation'; +import { ShareAssetType } from '@/api/asset_interfaces/share'; + +// Mock the hooks +jest.mock('@/api/buster_rest/users', () => ({ + useGetUserFavorites: jest.fn(), + useUpdateUserFavorites: jest.fn(), + useDeleteUserFavorite: jest.fn() +})); + +jest.mock('next/navigation', () => ({ + useParams: jest.fn() +})); + +// Do not mock useMemoizedFn to use the real implementation + +describe('useFavoriteSidebarPanel', () => { + const mockFavorites = [ + { id: 'metric1', name: 'Metric 1', asset_type: ShareAssetType.METRIC }, + { id: 'dashboard1', name: 'Dashboard 1', asset_type: ShareAssetType.DASHBOARD }, + { id: 'chat1', name: 'Chat 1', asset_type: ShareAssetType.CHAT }, + { id: 'collection1', name: 'Collection 1', asset_type: ShareAssetType.COLLECTION } + ]; + + const mockUpdateFavorites = jest.fn(); + const mockDeleteFavorite = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + + (useGetUserFavorites as jest.Mock).mockReturnValue({ + data: mockFavorites + }); + + (useUpdateUserFavorites as jest.Mock).mockReturnValue({ + mutateAsync: mockUpdateFavorites + }); + + (useDeleteUserFavorite as jest.Mock).mockReturnValue({ + mutateAsync: mockDeleteFavorite + }); + + (useParams as jest.Mock).mockReturnValue({ + chatId: undefined, + metricId: undefined, + dashboardId: undefined, + collectionId: undefined + }); + }); + + test('should return correct initial structure', () => { + const { result } = renderHook(() => useFavoriteSidebarPanel()); + + expect(result.current).toHaveProperty('favoritesDropdownItems'); + expect(result.current).toHaveProperty('favoritedPageType'); + }); + + test('should call updateUserFavorites when onFavoritesReorder is called', () => { + const { result } = renderHook(() => useFavoriteSidebarPanel()); + const itemIds = ['metric1', 'dashboard1']; + + act(() => { + const onItemsReorder = result.current.favoritesDropdownItems?.onItemsReorder; + if (onItemsReorder) { + onItemsReorder(itemIds); + } + }); + + expect(mockUpdateFavorites).toHaveBeenCalledWith(itemIds); + }); + + test('should correctly identify active chat asset', () => { + (useParams as jest.Mock).mockReturnValue({ + chatId: 'chat1' + }); + + const { result } = renderHook(() => useFavoriteSidebarPanel()); + + const chatItem = result.current.favoritesDropdownItems?.items.find( + (item) => item.id === 'chat1' + ); + + expect(chatItem?.active).toBe(true); + }); + + test('should correctly identify active metric asset', () => { + (useParams as jest.Mock).mockReturnValue({ + metricId: 'metric1' + }); + + const { result } = renderHook(() => useFavoriteSidebarPanel()); + + const metricItem = result.current.favoritesDropdownItems?.items.find( + (item) => item.id === 'metric1' + ); + + expect(metricItem?.active).toBe(true); + }); + + test('should set favoritedPageType to METRIC when metricId is in favorites', () => { + (useParams as jest.Mock).mockReturnValue({ + metricId: 'metric1' + }); + + const { result } = renderHook(() => useFavoriteSidebarPanel()); + + expect(result.current.favoritedPageType).toBe(ShareAssetType.METRIC); + }); + + test('should set favoritedPageType to null when page is not favorited', () => { + (useParams as jest.Mock).mockReturnValue({ + metricId: 'nonexistent' + }); + + const { result } = renderHook(() => useFavoriteSidebarPanel()); + + expect(result.current.favoritedPageType).toBe(null); + }); + + test('should return null for favoritesDropdownItems when no favorites exist', () => { + (useGetUserFavorites as jest.Mock).mockReturnValue({ + data: [] + }); + + const { result } = renderHook(() => useFavoriteSidebarPanel()); + + expect(result.current.favoritesDropdownItems).toBe(null); + }); + + test('should call deleteUserFavorite when item removal is triggered', () => { + const { result } = renderHook(() => useFavoriteSidebarPanel()); + + act(() => { + const firstItem = result.current.favoritesDropdownItems?.items[0]; + if (firstItem?.onRemove) { + firstItem.onRemove(); + } + }); + + expect(mockDeleteFavorite).toHaveBeenCalledWith(['metric1']); + }); + + test('should set favoritedPageType to null when chatId and another param exist', () => { + (useParams as jest.Mock).mockReturnValue({ + chatId: 'chat1', + metricId: 'metric1' + }); + + const { result } = renderHook(() => useFavoriteSidebarPanel()); + + expect(result.current.favoritedPageType).toBe(null); + }); +}); diff --git a/web/src/components/features/sidebars/useFavoritesSidebarPanel.tsx b/web/src/components/features/sidebars/useFavoritesSidebarPanel.tsx new file mode 100644 index 000000000..55e534690 --- /dev/null +++ b/web/src/components/features/sidebars/useFavoritesSidebarPanel.tsx @@ -0,0 +1,106 @@ +import type { BusterUserFavorite } from '@/api/asset_interfaces/users'; +import { ISidebarGroup } from '@/components/ui/sidebar'; +import { assetTypeToIcon, assetTypeToRoute } from '../config/assetIcons'; +import { useMemoizedFn } from '@/hooks'; +import { + useDeleteUserFavorite, + useGetUserFavorites, + useUpdateUserFavorites +} from '@/api/buster_rest/users'; +import { useMemo } from 'react'; +import { useParams } from 'next/navigation'; +import { ShareAssetType } from '@/api/asset_interfaces/share'; + +export const useFavoriteSidebarPanel = () => { + const { data: favorites } = useGetUserFavorites(); + const { mutateAsync: updateUserFavorites } = useUpdateUserFavorites(); + const { mutateAsync: deleteUserFavorite } = useDeleteUserFavorite(); + + const { chatId, metricId, dashboardId, collectionId } = useParams() as { + chatId: string | undefined; + metricId: string | undefined; + dashboardId: string | undefined; + collectionId: string | undefined; + }; + + const onFavoritesReorder = useMemoizedFn((itemIds: string[]) => { + updateUserFavorites(itemIds); + }); + + const isAssetActive = useMemoizedFn((favorite: BusterUserFavorite) => { + const assetType = favorite.asset_type; + const id = favorite.id; + + switch (assetType) { + case ShareAssetType.CHAT: + return id === chatId; + case ShareAssetType.METRIC: + return id === metricId; + case ShareAssetType.DASHBOARD: + return id === dashboardId; + case ShareAssetType.COLLECTION: + return id === collectionId; + default: + const _exhaustiveCheck: never = assetType; + return false; + } + }); + + const favoritedPageType: ShareAssetType | null = useMemo(() => { + if (chatId && (metricId || dashboardId || collectionId)) { + return null; + } + + if (chatId && favorites.some((f) => f.id === chatId)) { + return ShareAssetType.CHAT; + } + + if (metricId && favorites.some((f) => f.id === metricId)) { + return ShareAssetType.METRIC; + } + + if (dashboardId && favorites.some((f) => f.id === dashboardId)) { + return ShareAssetType.DASHBOARD; + } + + if (collectionId && favorites.some((f) => f.id === collectionId)) { + return ShareAssetType.COLLECTION; + } + + return null; + }, [favorites, chatId, metricId, dashboardId, collectionId]); + + const favoritesDropdownItems: ISidebarGroup | null = useMemo(() => { + if (!favorites || favorites.length === 0) return null; + + return { + label: 'Favorites', + id: 'favorites', + isSortable: true, + onItemsReorder: onFavoritesReorder, + items: favorites.map((favorite) => { + const Icon = assetTypeToIcon(favorite.asset_type); + const route = assetTypeToRoute(favorite.asset_type, favorite.id); + return { + label: favorite.name, + icon: , + route, + active: isAssetActive(favorite), + id: favorite.id, + onRemove: () => deleteUserFavorite([favorite.id]) + }; + }) + } satisfies ISidebarGroup; + }, [ + favorites, + deleteUserFavorite, + onFavoritesReorder, + isAssetActive, + chatId, + metricId, + dashboardId, + collectionId + ]); + + return { favoritesDropdownItems, favoritedPageType }; +}; diff --git a/web/src/components/ui/dropdown/Dropdown.tsx b/web/src/components/ui/dropdown/Dropdown.tsx index ecb515537..c3e715885 100644 --- a/web/src/components/ui/dropdown/Dropdown.tsx +++ b/web/src/components/ui/dropdown/Dropdown.tsx @@ -458,7 +458,8 @@ const DropdownItem = ({ checked={selected} disabled={disabled} onClick={onClickItem} - closeOnSelect={closeOnSelect}> + closeOnSelect={closeOnSelect} + dataTestId={`dropdown-checkbox-${value}`}> {renderContent()} ); diff --git a/web/src/components/ui/dropdown/DropdownBase.tsx b/web/src/components/ui/dropdown/DropdownBase.tsx index dacd11aa3..9745e0d47 100644 --- a/web/src/components/ui/dropdown/DropdownBase.tsx +++ b/web/src/components/ui/dropdown/DropdownBase.tsx @@ -163,12 +163,23 @@ const DropdownMenuCheckboxItemMultiple = React.forwardRef< React.ComponentPropsWithoutRef & { closeOnSelect?: boolean; selectType?: boolean; + dataTestId?: string; } >( ( - { className, children, onClick, checked = false, closeOnSelect = true, selectType, ...props }, + { + className, + children, + onClick, + checked = false, + closeOnSelect = true, + selectType, + dataTestId, + ...props + }, ref ) => { + console.log('dataTestId', dataTestId); return ( { + // Create test dates based on current time + const now = dayjs(); + const today = now.format(); + const yesterday = now.subtract(1, 'day').format(); + const threeDaysAgo = now.subtract(3, 'day').format(); + const tenDaysAgo = now.subtract(10, 'day').format(); + + // More precise yesterday times for edge case testing + const yesterdayStart = now.subtract(1, 'day').startOf('day').format(); + const yesterdayMiddle = now.subtract(1, 'day').hour(12).minute(0).second(0).format(); + const yesterdayEnd = now.subtract(1, 'day').endOf('day').format(); + + test('should return empty buckets when input array is empty', () => { + const result = createChatRecord([]); + + expect(result).toEqual({ + TODAY: [], + YESTERDAY: [], + LAST_WEEK: [], + ALL_OTHERS: [] + }); + }); + + test('should categorize items into correct buckets', () => { + const mockData = [ + { id: '1', last_edited: today }, + { id: '2', last_edited: yesterday }, + { id: '3', last_edited: threeDaysAgo }, + { id: '4', last_edited: tenDaysAgo } + ]; + + const result = createChatRecord(mockData); + + // Check TODAY bucket + expect(result.TODAY.length).toBe(1); + expect(result.TODAY[0].id).toBe('1'); + + // Check YESTERDAY bucket + expect(result.YESTERDAY.length).toBe(1); + expect(result.YESTERDAY[0].id).toBe('2'); + + // Check LAST_WEEK bucket + expect(result.LAST_WEEK.length).toBe(1); + expect(result.LAST_WEEK[0].id).toBe('3'); + + // Check ALL_OTHERS bucket + expect(result.ALL_OTHERS.length).toBe(1); + expect(result.ALL_OTHERS[0].id).toBe('4'); + }); + + test('should place all items in ALL_OTHERS when all are older than a week', () => { + const mockData = [ + { id: '1', last_edited: tenDaysAgo }, + { id: '2', last_edited: now.subtract(15, 'day').format() }, + { id: '3', last_edited: now.subtract(30, 'day').format() } + ]; + + const result = createChatRecord(mockData); + + expect(result.TODAY).toHaveLength(0); + expect(result.YESTERDAY).toHaveLength(0); + expect(result.LAST_WEEK).toHaveLength(0); + expect(result.ALL_OTHERS).toHaveLength(3); + expect(result.ALL_OTHERS.map((item) => item.id)).toEqual(['1', '2', '3']); + }); + + test('should handle items with extended properties', () => { + // Create an item with additional properties beyond the required id and last_edited + const extendedItem = { + id: '1', + last_edited: today, + name: 'Test Item', + created_by: 'user-123', + updated_at: '2025-04-22T20:40:31.672893+00:00', + extra_field: 'some value' + }; + + const result = createChatRecord([extendedItem]); + + // Verify the item is placed in TODAY bucket and preserves all properties + expect(result.TODAY).toHaveLength(1); + expect(result.TODAY[0]).toEqual(extendedItem); + expect(result.TODAY[0].name).toBe('Test Item'); + expect(result.TODAY[0].extra_field).toBe('some value'); + }); + + test('should place multiple items from yesterday in the YESTERDAY bucket', () => { + const mockData = [ + { id: 'y1', last_edited: yesterday }, + { id: 'y2', last_edited: yesterdayStart }, + { id: 'y3', last_edited: yesterdayMiddle }, + { id: 'y4', last_edited: yesterdayEnd } + ]; + + const result = createChatRecord(mockData); + + expect(result.TODAY).toHaveLength(0); + expect(result.YESTERDAY).toHaveLength(4); + expect(result.LAST_WEEK).toHaveLength(0); + expect(result.ALL_OTHERS).toHaveLength(0); + + // Verify all IDs are in the YESTERDAY bucket + const yesterdayIds = result.YESTERDAY.map((item) => item.id); + expect(yesterdayIds).toContain('y1'); + expect(yesterdayIds).toContain('y2'); + expect(yesterdayIds).toContain('y3'); + expect(yesterdayIds).toContain('y4'); + }); + + test('should handle boundary cases for yesterday time', () => { + // Create dates right at the boundary of yesterday/today + const almostToday = now.startOf('day').subtract(1, 'millisecond').format(); + const barelyToday = now.startOf('day').format(); + + const mockData = [ + { id: 'still-yesterday', last_edited: almostToday }, + { id: 'barely-today', last_edited: barelyToday } + ]; + + const result = createChatRecord(mockData); + + expect(result.YESTERDAY).toHaveLength(1); + expect(result.YESTERDAY[0].id).toBe('still-yesterday'); + + expect(result.TODAY).toHaveLength(1); + expect(result.TODAY[0].id).toBe('barely-today'); + }); + + test('should sort items correctly when mixed with other time periods', () => { + // Create a mix of items with some yesterday dates mixed in + const mockData = [ + { id: 'today-1', last_edited: today }, + { id: 'yesterday-1', last_edited: yesterday }, + { id: 'last-week', last_edited: threeDaysAgo }, + { id: 'yesterday-2', last_edited: yesterdayEnd }, + { id: 'old-item', last_edited: tenDaysAgo }, + { id: 'yesterday-3', last_edited: yesterdayStart }, + { id: 'today-2', last_edited: now.format() } + ]; + + const result = createChatRecord(mockData); + + // Verify correct counts in each bucket + expect(result.TODAY).toHaveLength(2); + expect(result.YESTERDAY).toHaveLength(3); + expect(result.LAST_WEEK).toHaveLength(1); + expect(result.ALL_OTHERS).toHaveLength(1); + + // Verify all yesterday items are in the YESTERDAY bucket + const yesterdayIds = result.YESTERDAY.map((item) => item.id); + expect(yesterdayIds).toContain('yesterday-1'); + expect(yesterdayIds).toContain('yesterday-2'); + expect(yesterdayIds).toContain('yesterday-3'); + }); +}); diff --git a/web/src/components/ui/list/createChatRecord.ts b/web/src/components/ui/list/createChatRecord.ts new file mode 100644 index 000000000..a6a6449da --- /dev/null +++ b/web/src/components/ui/list/createChatRecord.ts @@ -0,0 +1,70 @@ +import { getNow, isDateAfter, isDateBefore, isDateSame } from '@/lib/date'; + +type ListItem = { + id: string; + last_edited: string; +}; + +export const createChatRecord = ( + data: T[] +): { + TODAY: T[]; + YESTERDAY: T[]; + LAST_WEEK: T[]; + ALL_OTHERS: T[]; +} => { + const today = getNow(); + const yesterday = today.subtract(1, 'day'); + const weekStartDate = today.subtract(8, 'day').startOf('day'); + const twoDaysAgo = today.subtract(1, 'day').startOf('day'); + + const TODAY: T[] = []; + const YESTERDAY: T[] = []; + const LAST_WEEK: T[] = []; + const ALL_OTHERS: T[] = []; + + // Loop through the data array only once + data.forEach((item) => { + if ( + isDateSame({ + date: item.last_edited, + compareDate: today, + interval: 'day' + }) + ) { + TODAY.push(item); + } else if ( + isDateSame({ + date: item.last_edited, + compareDate: yesterday, + interval: 'day' + }) + ) { + YESTERDAY.push(item); + } else if ( + isDateAfter({ + date: item.last_edited, + compareDate: weekStartDate, + interval: 'day' + }) && + isDateBefore({ + date: item.last_edited, + compareDate: twoDaysAgo, + interval: 'day' + }) + ) { + LAST_WEEK.push(item); + } else { + ALL_OTHERS.push(item); + } + }); + + const result = { + TODAY, + YESTERDAY, + LAST_WEEK, + ALL_OTHERS + }; + + return result; +}; diff --git a/web/src/components/ui/list/useCreateListByDate.ts b/web/src/components/ui/list/useCreateListByDate.ts index 7cb766ee7..ba04b0734 100644 --- a/web/src/components/ui/list/useCreateListByDate.ts +++ b/web/src/components/ui/list/useCreateListByDate.ts @@ -1,61 +1,11 @@ -import { getNow, isDateAfter, isDateBefore, isDateSame } from '@/lib/date'; import { useMemo } from 'react'; +import { createChatRecord } from './createChatRecord'; type ListItem = { id: string; last_edited: string; }; -const createChatRecord = ( - data: T[] -): { - TODAY: T[]; - YESTERDAY: T[]; - LAST_WEEK: T[]; - ALL_OTHERS: T[]; -} => { - const today = getNow(); - const TODAY = data.filter((d) => - isDateSame({ - date: d.last_edited, - compareDate: today, - interval: 'day' - }) - ); - const YESTERDAY = data.filter((d) => - isDateSame({ - date: d.last_edited, - compareDate: today.subtract(1, 'day'), - interval: 'day' - }) - ); - const LAST_WEEK = data.filter( - (d) => - isDateBefore({ - date: d.last_edited, - compareDate: today.subtract(2, 'day').startOf('day'), - interval: 'day' - }) && - isDateAfter({ - date: d.last_edited, - compareDate: today.subtract(8, 'day').startOf('day'), - interval: 'day' - }) - ); - const ALL_OTHERS = data.filter( - (d) => !TODAY.includes(d) && !YESTERDAY.includes(d) && !LAST_WEEK.includes(d) - ); - - return { - TODAY, - YESTERDAY, - LAST_WEEK, - ALL_OTHERS - }; -}; - export const useCreateListByDate = ({ data }: { data: T[] }) => { - const listRecord = useMemo(() => createChatRecord(data), [data]); - - return listRecord; + return useMemo(() => createChatRecord(data), [data]); }; diff --git a/web/src/components/ui/sidebar/Sidebar.stories.tsx b/web/src/components/ui/sidebar/Sidebar.stories.tsx index 8adec438e..6d1d44534 100644 --- a/web/src/components/ui/sidebar/Sidebar.stories.tsx +++ b/web/src/components/ui/sidebar/Sidebar.stories.tsx @@ -72,8 +72,7 @@ export const Default: Story = { content: mockGroupedContent, footer: (
Footer
- ), - activeItem: '1' + ) } }; @@ -88,27 +87,25 @@ export const WithLongContent: Story = { id: `item-${i}`, label: `Menu Item ${i + 1}`, icon: , - route: BusterRoutes.APP_HOME + route: BusterRoutes.APP_HOME, + active: i === 0 })) } ], - footer:
Sticky Footer
, - activeItem: 'item-1' + footer:
Sticky Footer
} }; export const NoFooter: Story = { args: { header:
My App
, - content: mockGroupedContent, - activeItem: '1' + content: mockGroupedContent } }; export const ScrollAndTruncationTest: Story = { args: { header:
Scroll & Truncation Test
, - activeItem: 'long-4', content: [ { id: 'default-items', @@ -121,7 +118,8 @@ export const ScrollAndTruncationTest: Story = { id: `short-${i}`, label: `Item ${i + 1}`, icon: , - route: BusterRoutes.APP_HOME + route: BusterRoutes.APP_HOME, + active: i === 4 })) }, { @@ -175,7 +173,6 @@ export const WithRemovableItems: Story = { items: mockItems } ], - activeItem: '1', footer:
Footer
} }; diff --git a/web/src/components/ui/sidebar/Sidebar.tsx b/web/src/components/ui/sidebar/Sidebar.tsx index ac3b2d1d8..6ca78b54e 100644 --- a/web/src/components/ui/sidebar/Sidebar.tsx +++ b/web/src/components/ui/sidebar/Sidebar.tsx @@ -3,46 +3,42 @@ import { ISidebarGroup, ISidebarList, SidebarProps } from './interfaces'; import { SidebarCollapsible } from './SidebarCollapsible'; import { SidebarItem } from './SidebarItem'; -export const Sidebar: React.FC = React.memo( - ({ header, content, footer, activeItem }) => { - return ( -
-
-
{header}
-
- {content.map((item) => ( - - ))} -
+export const Sidebar: React.FC = React.memo(({ header, content, footer }) => { + return ( +
+
+
{header}
+
+ {content.map((item) => ( + + ))}
- {footer &&
{footer}
}
- ); - } -); + {footer &&
{footer}
} +
+ ); +}); Sidebar.displayName = 'Sidebar'; const ContentSelector: React.FC<{ content: SidebarProps['content'][number]; - activeItem: SidebarProps['activeItem']; -}> = React.memo(({ content, activeItem }) => { +}> = React.memo(({ content }) => { if (isSidebarGroup(content)) { - return ; + return ; } - return ; + return ; }); ContentSelector.displayName = 'ContentSelector'; const SidebarList: React.FC<{ items: ISidebarList['items']; - activeItem: SidebarProps['activeItem']; -}> = ({ items, activeItem }) => { +}> = ({ items }) => { return (
{items.map((item) => ( - + ))}
); diff --git a/web/src/components/ui/sidebar/interfaces.ts b/web/src/components/ui/sidebar/interfaces.ts index 9fc096aae..4c7c18748 100644 --- a/web/src/components/ui/sidebar/interfaces.ts +++ b/web/src/components/ui/sidebar/interfaces.ts @@ -33,6 +33,5 @@ export interface SidebarProps { header: React.ReactNode; content: SidebarContent[]; footer?: React.ReactNode; - activeItem: string; isSortable?: boolean; }