#![allow(dead_code)]
use axum::{
Router,
body::Body,
extract::ws::{Message, WebSocket, WebSocketUpgrade},
http::{HeaderMap, Response, StatusCode},
routing::get,
};
use std::net::SocketAddr;
use tokio::task::JoinHandle;
pub struct TestServer {
addr: SocketAddr,
handle: JoinHandle<()>,
}
impl TestServer {
pub async fn start() -> Self {
let app = Router::new()
.route("/", get(index_page))
.route("/button.html", get(button_page))
.route("/form.html", get(form_page))
.route("/input.html", get(input_page))
.route("/dblclick.html", get(dblclick_page))
.route("/keyboard.html", get(keyboard_page))
.route("/locator.html", get(locator_page))
.route("/locators.html", get(locators_page))
.route("/checkbox.html", get(checkbox_page))
.route("/hover.html", get(hover_page))
.route("/select.html", get(select_page))
.route("/upload.html", get(upload_page))
.route("/keyboard_mouse.html", get(keyboard_mouse_page))
.route("/click_options.html", get(click_options_page))
.route("/text.html", get(text_page))
.route("/websocket.html", get(websocket_page))
.route("/anchors.html", get(anchors_page))
.route("/filter.html", get(filter_page))
.route("/ws", get(ws_handler))
.route("/frame.html", get(frame_handler))
.route("/focus_blur.html", get(focus_blur_page))
.route("/all_texts.html", get(all_texts_page))
.route("/echo-headers", get(echo_headers_page))
.route("/drag_drop.html", get(drag_drop_page))
.route("/wait_for.html", get(wait_for_page))
.route("/api/data.json", get(json_data_endpoint))
.route("/redirect", get(redirect_handler))
.route("/iframe-test.html", get(iframe_test_page))
.route("/iframe-content.html", get(iframe_content_page))
.route("/iframe-content2.html", get(iframe_content2_page))
.route("/nested-iframe.html", get(nested_iframe_page))
.route("/inner-iframe.html", get(inner_iframe_page));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("Failed to bind test server");
let addr = listener.local_addr().expect("Failed to get local address");
let handle = tokio::spawn(async move {
axum::serve(listener, app)
.await
.expect("Test server failed");
});
TestServer { addr, handle }
}
pub fn url(&self) -> String {
format!("http://{}", self.addr)
}
pub fn shutdown(self) {
self.handle.abort();
}
}
async fn index_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>Test Index</title></head>
<body>
<h1>Test Page</h1>
<p>This is a test paragraph.</p>
<a href="/button.html">Go to button page</a>
</body>
</html>"#,
))
.unwrap()
}
async fn button_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>Button Test</title></head>
<body>
<button id="btn" onclick="this.textContent='clicked'">Click me</button>
<button id="btn2" onclick="this.textContent='clicked 2'">Click me 2</button>
</body>
</html>"#,
))
.unwrap()
}
async fn form_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>Form Test</title></head>
<body>
<form>
<input type="text" id="name" name="name" />
<textarea id="bio" name="bio"></textarea>
<input type="submit" value="Submit" />
</form>
</body>
</html>"#,
))
.unwrap()
}
async fn input_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>Input Test</title></head>
<body>
<input type="text" id="input" value="initial" />
<input type="text" id="empty" value="" />
</body>
</html>"#,
))
.unwrap()
}
async fn dblclick_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>Double Click Test</title></head>
<body>
<div id="target" ondblclick="this.textContent='double clicked'">Double click me</div>
</body>
</html>"#,
))
.unwrap()
}
async fn keyboard_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>Keyboard Test</title></head>
<body>
<input type="text" id="input" onkeydown="if(event.key==='Enter') this.value='submitted'" />
</body>
</html>"#,
))
.unwrap()
}
async fn locator_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>Locator Test</title></head>
<body>
<h1>Test Page</h1>
<p id="p1">First paragraph</p>
<p id="p2">Second paragraph</p>
<p id="p3">Third paragraph</p>
<div class="container">
<span id="nested">Nested element</span>
</div>
<div id="hidden" style="display: none;">Hidden element</div>
<button>Submit</button>
<button>Submit Order</button>
<span>Hello World</span>
<span>hello world</span>
<div class="text-container">
<span>Inner Text</span>
</div>
<label for="email">Email Address</label>
<input id="email" type="text" placeholder="Enter your email" />
<label for="name">Full Name</label>
<input id="name" type="text" placeholder="Enter your name" />
<img src="logo.png" alt="Company Logo" />
<img src="banner.png" alt="Welcome Banner" />
<span title="More Info">Details</span>
<span title="More Info Expanded">Extended Details</span>
<button data-testid="submit-btn">Submit Form</button>
<button data-testid="cancel-btn">Cancel</button>
<!-- get_by_role test elements -->
<nav aria-label="Main">
<a href="/home">Home</a>
<a href="/about">About</a>
</nav>
<h2>Section Title</h2>
<h3>Subsection</h3>
<input type="checkbox" aria-label="I agree" checked />
<input type="checkbox" aria-label="Subscribe" />
<button disabled>Disabled Button</button>
<div role="alert">Important message</div>
</body>
</html>"#,
))
.unwrap()
}
async fn locators_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>Locators Test</title></head>
<body>
<h1>Test Page</h1>
<p id="p1">First paragraph</p>
<p id="p2">Second paragraph</p>
<p id="p3">Third paragraph</p>
<p id="p4">Fourth paragraph</p>
<div class="container">
<span id="nested">Nested element</span>
</div>
<div id="hidden" style="display: none;">Hidden element</div>
</body>
</html>"#,
))
.unwrap()
}
async fn checkbox_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>Checkbox Test</title></head>
<body>
<input type="checkbox" id="checkbox" />
<label for="checkbox">Unchecked checkbox</label>
<br />
<input type="checkbox" id="checked-checkbox" checked />
<label for="checked-checkbox">Checked checkbox</label>
<br />
<input type="radio" id="radio1" name="radio-group" />
<label for="radio1">Radio 1</label>
<br />
<input type="radio" id="radio2" name="radio-group" />
<label for="radio2">Radio 2</label>
</body>
</html>"#,
))
.unwrap()
}
async fn hover_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head>
<title>Hover Test</title>
<style>
#hover-button {
padding: 10px;
background-color: #ccc;
}
#tooltip {
display: none;
margin-top: 10px;
padding: 5px;
background-color: yellow;
}
#hover-button:hover + #tooltip {
display: block;
}
</style>
</head>
<body>
<button id="hover-button">Hover over me</button>
<div id="tooltip">This is a tooltip</div>
</body>
</html>"#,
))
.unwrap()
}
async fn select_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>Select Test</title></head>
<body>
<select id="single-select">
<option value="">--Please choose an option--</option>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
<option value="cherry">Cherry</option>
</select>
<br /><br />
<select id="multi-select" multiple>
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
<option value="yellow">Yellow</option>
</select>
<br /><br />
<select id="select-by-index">
<option>First</option>
<option>Second</option>
<option>Third</option>
</select>
</body>
</html>"#,
))
.unwrap()
}
async fn upload_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>File Upload Test</title></head>
<body>
<input type="file" id="single-file" />
<br /><br />
<input type="file" id="multi-file" multiple />
<br /><br />
<div id="file-info"></div>
<script>
document.getElementById('single-file').addEventListener('change', (e) => {
const files = Array.from(e.target.files).map(f => f.name).join(', ');
document.getElementById('file-info').textContent = 'Single: ' + files;
});
document.getElementById('multi-file').addEventListener('change', (e) => {
const files = Array.from(e.target.files).map(f => f.name).join(', ');
document.getElementById('file-info').textContent = 'Multiple: ' + files;
});
</script>
</body>
</html>"#,
))
.unwrap()
}
async fn keyboard_mouse_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>Keyboard and Mouse Test</title></head>
<body>
<h1>Keyboard and Mouse Testing</h1>
<input type="text" id="keyboard-input" placeholder="Type here" />
<div id="keyboard-result"></div>
<div id="clickable" style="width: 300px; height: 300px; background-color: lightblue; margin-top: 20px;">
Click or double-click me
</div>
<div id="mouse-result"></div>
<div id="mouse-coords"></div>
<script>
// Keyboard event handlers
document.getElementById('keyboard-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
document.getElementById('keyboard-result').textContent = 'Enter pressed';
}
});
// Mouse event handlers
document.getElementById('clickable').addEventListener('click', (e) => {
document.getElementById('mouse-result').textContent = 'Clicked';
});
document.getElementById('clickable').addEventListener('dblclick', (e) => {
document.getElementById('mouse-result').textContent = 'Double-clicked';
});
document.addEventListener('mousemove', (e) => {
document.getElementById('mouse-coords').textContent = `Mouse: (${e.clientX}, ${e.clientY})`;
});
</script>
</body>
</html>"#,
))
.unwrap()
}
async fn click_options_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>Click Options Test</title></head>
<body>
<button id="button">Click Me</button>
<button id="hidden-button" style="display: none;">Hidden Button</button>
<div id="result"></div>
<script>
const button = document.getElementById('button');
const hiddenButton = document.getElementById('hidden-button');
const result = document.getElementById('result');
// Track all mouse events
button.addEventListener('mousedown', (e) => {
const buttonName = e.button === 0 ? 'left' : e.button === 1 ? 'middle' : 'right';
result.textContent = `mousedown button:${buttonName} shiftKey:${e.shiftKey} ctrlKey:${e.ctrlKey}`;
});
button.addEventListener('click', (e) => {
const buttonName = e.button === 0 ? 'left' : e.button === 1 ? 'middle' : 'right';
result.textContent = `click button:${buttonName} shiftKey:${e.shiftKey} ctrlKey:${e.ctrlKey}`;
});
button.addEventListener('contextmenu', (e) => {
e.preventDefault(); // Prevent context menu
result.textContent = `contextmenu (right) shiftKey:${e.shiftKey} ctrlKey:${e.ctrlKey}`;
});
button.addEventListener('auxclick', (e) => {
const buttonName = e.button === 1 ? 'middle' : e.button === 2 ? 'right' : 'other';
result.textContent = `auxclick button:${buttonName} shiftKey:${e.shiftKey} ctrlKey:${e.ctrlKey}`;
});
button.addEventListener('dblclick', (e) => {
result.textContent = 'dblclick';
});
hiddenButton.addEventListener('click', (e) => {
result.textContent = 'hidden-button-clicked';
});
</script>
</body>
</html>"#,
))
.unwrap()
}
async fn text_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>Text Assertions Test</title></head>
<body>
<h1>Welcome to Playwright</h1>
<p id="whitespace">
Text with whitespace
</p>
<p id="long-text">This is the beginning and middle of the text and the end.</p>
<input type="text" id="name-input" value="John Doe" />
<input type="text" id="empty-input" value="" />
</body>
</html>"#,
))
.unwrap()
}
async fn websocket_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>WebSocket Test</title></head>
<body>
<h1>WebSocket Test</h1>
<div id="log"></div>
<script>
const log = document.getElementById('log');
const ws = new WebSocket('ws://' + location.host + '/ws');
ws.onopen = () => {
log.textContent += 'open\n';
ws.send('Hello Server');
};
ws.onmessage = (event) => {
log.textContent += 'received: ' + event.data + '\n';
};
ws.onclose = () => {
log.textContent += 'closed\n';
};
</script>
</body>
</html>"#,
))
.unwrap()
}
async fn anchors_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
"<!DOCTYPE html>
<html>
<head><title>Anchor Navigation Test</title></head>
<body>
<h1>Anchor Navigation Test Page</h1>
<nav>
<a id=\"link-to-section1\" href=\"#section1\">Go to Section 1</a> |
<a id=\"link-to-section2\" href=\"#section2\">Go to Section 2</a> |
<a id=\"link-to-section3\" href=\"#section3\">Go to Section 3</a>
</nav>
<section id=\"section1\" style=\"margin-top: 50px; padding: 20px; background: #f0f0f0;\">
<h2>Section 1</h2>
<p>This is section 1. The URL should include #section1 when you navigate here.</p>
</section>
<section id=\"section2\" style=\"margin-top: 50px; padding: 20px; background: #e0e0e0;\">
<h2>Section 2</h2>
<p>This is section 2. The URL should include #section2 when you navigate here.</p>
</section>
<section id=\"section3\" style=\"margin-top: 50px; padding: 20px; background: #d0d0d0;\">
<h2>Section 3</h2>
<p>This is section 3. The URL should include #section3 when you navigate here.</p>
</section>
</body>
</html>",
))
.unwrap()
}
async fn filter_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>Filter Test</title></head>
<body>
<nav>
<a class="nav-link" href="/home">Home</a>
<a class="nav-link" href="/about">About</a>
</nav>
<table>
<thead>
<tr><th>Fruit</th><th>Price</th><th>Action</th></tr>
</thead>
<tbody>
<!-- Row 1: Apple with action button -->
<tr class="data-row">
<td>Apple</td>
<td>$1.00</td>
<td><button class="action-btn">Buy</button></td>
</tr>
<!-- Row 2: Banana with action button -->
<tr class="data-row">
<td>Banana</td>
<td>$0.50</td>
<td><button class="action-btn">Buy</button></td>
</tr>
<!-- Row 3: Cherry without action button (out of stock) -->
<tr class="data-row">
<td>Cherry</td>
<td>$2.00</td>
<td><span class="out-of-stock">Out of stock</span></td>
</tr>
</tbody>
</table>
<button class="delete-btn">Delete All</button>
</body>
</html>"#,
))
.unwrap()
}
async fn focus_blur_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>Focus Blur Test</title></head>
<body>
<input type="text" id="input1" />
<input type="text" id="input2" />
<button id="btn">Button</button>
</body>
</html>"#,
))
.unwrap()
}
async fn all_texts_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>All Texts Test</title></head>
<body>
<li class="item">Alpha</li>
<li class="item">Beta</li>
<li class="item">Gamma</li>
</body>
</html>"#,
))
.unwrap()
}
async fn drag_drop_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head>
<title>Drag and Drop Test</title>
<style>
#source {
width: 80px; height: 80px;
background: steelblue;
position: absolute; top: 50px; left: 50px;
cursor: grab;
}
#target {
width: 120px; height: 120px;
background: lightgreen;
position: absolute; top: 50px; left: 250px;
border: 2px dashed #666;
}
#target.dropped { background: gold; }
#result { position: absolute; top: 220px; left: 50px; }
</style>
</head>
<body>
<div id="source" draggable="true">Drag me</div>
<div id="target">Drop here</div>
<div id="result">no drop</div>
<script>
var source = document.getElementById('source');
var target = document.getElementById('target');
var result = document.getElementById('result');
source.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('text/plain', 'dragged');
});
target.addEventListener('dragover', function(e) {
e.preventDefault();
});
target.addEventListener('drop', function(e) {
e.preventDefault();
target.classList.add('dropped');
result.textContent = 'dropped';
});
</script>
</body>
</html>"#,
))
.unwrap()
}
async fn wait_for_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>Wait For Test</title></head>
<body>
<div id="visible-element" style="display: block;">I am visible</div>
<div id="hidden-element" style="display: none;">I am hidden</div>
<div id="container"></div>
<script>
// showElement: makes #hidden-element visible after a delay
window.showElement = function(delayMs) {
setTimeout(function() {
document.getElementById('hidden-element').style.display = 'block';
}, delayMs);
};
// hideElement: hides #visible-element after a delay
window.hideElement = function(delayMs) {
setTimeout(function() {
document.getElementById('visible-element').style.display = 'none';
}, delayMs);
};
// appendElement: appends a new div#dynamic-element after a delay
window.appendElement = function(delayMs) {
setTimeout(function() {
var el = document.createElement('div');
el.id = 'dynamic-element';
el.textContent = 'I was appended';
document.getElementById('container').appendChild(el);
}, delayMs);
};
// removeElement: removes #visible-element after a delay
window.removeElement = function(delayMs) {
setTimeout(function() {
var el = document.getElementById('visible-element');
if (el) el.parentNode.removeChild(el);
}, delayMs);
};
</script>
</body>
</html>"#,
))
.unwrap()
}
async fn ws_handler(ws: WebSocketUpgrade) -> impl axum::response::IntoResponse {
ws.on_upgrade(handle_socket)
}
async fn frame_handler() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(r#"<iframe src="/button.html"></iframe>"#))
.unwrap()
}
async fn handle_socket(mut socket: WebSocket) {
while let Some(msg) = socket.recv().await {
if let Ok(msg) = msg {
if let Message::Text(text) = msg {
let _ = socket.send(Message::Text(text)).await;
}
} else {
return;
}
}
}
async fn echo_headers_page(headers: HeaderMap) -> Response<Body> {
let mut map = serde_json::Map::new();
for (name, value) in &headers {
if let Ok(v) = value.to_str() {
map.insert(
name.as_str().to_lowercase(),
serde_json::Value::String(v.to_string()),
);
}
}
let json = serde_json::to_string(&map).unwrap_or_else(|_| "{}".to_string());
let html = format!(
r#"<!DOCTYPE html>
<html>
<head><title>Echo Headers</title></head>
<body>
<pre id="headers">{}</pre>
</body>
</html>"#,
html_escape(&json)
);
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(html))
.unwrap()
}
async fn json_data_endpoint() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(Body::from(
r#"{"status":"ok","message":"hello from test server"}"#,
))
.unwrap()
}
async fn iframe_test_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r##"<!DOCTYPE html>
<html>
<head><title>IFrame Test</title></head>
<body>
<h1>Main Page</h1>
<iframe name="content" src="/iframe-content.html" id="frame1" width="400" height="300"></iframe>
<iframe name="secondary" src="/iframe-content2.html" id="frame2" width="400" height="300"></iframe>
</body>
</html>"##,
))
.unwrap()
}
async fn iframe_content_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r##"<!DOCTYPE html>
<html>
<head><title>IFrame Content</title></head>
<body>
<h1>Inside Frame</h1>
<button id="frame-btn" onclick="this.textContent='clicked'">Click Me</button>
<label for="frame-input">Email</label>
<input id="frame-input" type="text" placeholder="Enter email" />
<img src="logo.png" alt="Frame Logo" />
<span title="Frame Tooltip">Info</span>
<button data-testid="frame-submit">Submit</button>
<nav>
<a href="#" role="link">Frame Link</a>
</nav>
</body>
</html>"##,
))
.unwrap()
}
async fn iframe_content2_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r#"<!DOCTYPE html>
<html>
<head><title>IFrame Content 2</title></head>
<body>
<h1>Second Frame</h1>
<button id="btn2" onclick="this.textContent='clicked2'">Other Button</button>
</body>
</html>"#,
))
.unwrap()
}
async fn nested_iframe_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r##"<!DOCTYPE html>
<html>
<head><title>Nested IFrame</title></head>
<body>
<h1>Outer Page</h1>
<iframe id="outer" src="/inner-iframe.html" width="500" height="400"></iframe>
</body>
</html>"##,
))
.unwrap()
}
async fn inner_iframe_page() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(Body::from(
r##"<!DOCTYPE html>
<html>
<head><title>Inner IFrame</title></head>
<body>
<h1>Inner Frame</h1>
<iframe id="innermost" src="/iframe-content.html" width="300" height="200"></iframe>
</body>
</html>"##,
))
.unwrap()
}
async fn redirect_handler() -> Response<Body> {
Response::builder()
.status(StatusCode::FOUND)
.header("Location", "/")
.body(Body::empty())
.unwrap()
}
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}