diff --git a/crates/docx/tests/fixtures.rs b/crates/docx/tests/fixtures.rs
index c4e5aee99..350a007b0 100644
--- a/crates/docx/tests/fixtures.rs
+++ b/crates/docx/tests/fixtures.rs
@@ -1609,6 +1609,88 @@ fn cover_page_group_fills_freeforms_and_text_box_margins() {
check(&wordcraft_docx::read(&wordcraft_docx::write(&d).unwrap()).unwrap());
}
+/// #886: saving dropped Word's DrawingML shapes (rectangles, ellipses, triangles, arrows,
+/// stars, connectors and their theme fills). Each kind the report lists is read, saved and read
+/// back with the same kind, fill, outline, size and position, and the saved part still holds a
+/// `wps:wsp` with its preset geometry.
+#[test]
+fn drawingml_shapes_survive_saving() {
+ let style = r#""#;
+ let geom = |prst: &str| format!(r#""#);
+ let presets = ["rect", "ellipse", "triangle", "rightArrow", "star5"];
+ let mut body: String = presets.iter().map(|p| anchored_wsp(p, 2009775, 819150, &geom(p), style, "")).collect();
+ body.push_str(&anchored_wsp(
+ "Connector",
+ 2362200,
+ 390525,
+ r#""#,
+ style,
+ "",
+ ));
+ body.push_str(&anchored_wsp(
+ "Text Box",
+ 2324100,
+ 571500,
+ r#""#,
+ "",
+ r#"Floating text box"#,
+ ));
+ let shapes = |d: &Document| {
+ paras(d)[0]
+ .objects
+ .iter()
+ .map(|o| match o {
+ InlineObject::Shape { kind, w, h, fill, stroke, stroke_width, float, extra, .. } => {
+ (*kind, *w, *h, *fill, *stroke, *stroke_width, float.x, float.y, float.wrap, extra.arrow_end)
+ }
+ o => panic!("{o:?}"),
+ })
+ .collect::>()
+ };
+ let d = read_body(&format!("{body}"));
+ let accent = d.settings.theme_colors[4];
+ let read = shapes(&d);
+ let kinds: Vec = read.iter().map(|s| s.0).collect();
+ assert_eq!(
+ kinds,
+ [
+ ShapeKind::Rectangle,
+ ShapeKind::Ellipse,
+ ShapeKind::Triangle,
+ ShapeKind::Arrow,
+ ShapeKind::Star,
+ ShapeKind::StraightConnector,
+ ShapeKind::TextBox
+ ]
+ );
+ for s in &read[..5] {
+ assert_eq!(s.3, Some(accent), "{:?} is filled from the theme", s.0);
+ assert!(s.4.is_some() && s.5 > 0.0, "{:?} has its outline", s.0);
+ assert_eq!((s.6, s.7, s.8), (1.0, 2.0, Wrap::InFrontOfText), "{:?} keeps its place", s.0);
+ }
+ assert!(read[5].9, "the connector keeps its arrowhead");
+ let bytes = wordcraft_docx::write(&d).unwrap();
+ let again = wordcraft_docx::read(&bytes).unwrap();
+ assert_eq!(shapes(&again), read, "every shape survives saving");
+ assert_eq!(part_text_box(&again), "Floating text box");
+ let mut z = zip::ZipArchive::new(std::io::Cursor::new(&bytes[..])).unwrap();
+ let mut xml = String::new();
+ std::io::Read::read_to_string(&mut z.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
+ assert_eq!(xml.matches("").count(), 7);
+ for p in presets.iter().chain(&["straightConnector1"]) {
+ assert!(xml.contains(&format!(r#""#)), "{p} is written");
+ }
+}
+
+/// The text of the first text box in `d`.
+fn part_text_box(d: &Document) -> String {
+ let story = paras(d)[0].objects.iter().find_map(|o| match o {
+ InlineObject::Shape { kind: ShapeKind::TextBox, story, .. } => *story,
+ _ => None,
+ });
+ story.and_then(|id| d.parts.get(&id)).map(|p| p.blocks.iter().filter_map(|b| b.as_para()).map(|p| p.plain_text()).collect()).unwrap_or_default()
+}
+
#[test]
fn embedded_fonts_survive_a_round_trip() {
let table = r#""#;