蓝桥杯模拟4期自动化测试代码完整版解析

文章目录

一、题目 II:自动化测试题目

本题总分:50 分

【前期准备】

请再次双击被测系统的 run.bat 脚本恢复原始数据并重新启动被测系统,然后在火狐浏览器中输入被测系统的访问地址:

  • 访问地址:http://localhost:8090
  • 用户名:admin
  • 密 码:admin123

注意:在做自动化题目时,最好重新启动被测系统,以免有脏数据干扰。如果重启后无法正常登录,请深度刷新浏览器再登录,或者重新解压缩被测系统源码包再次进行一键启动,启动后也再次刷新浏览器。

【题目描述】

请使用 Java 编程语言,结合 PO 模式(Page Object Model,页面对象模型) 思想,实现业务与测试用例分离。要求使用 Selenium 自动化测试工具补全如下 5 个 Java 类文件中 TODO 处缺失的代码:

  • Page1-用户登录 Page 类:LoginPage.java(非考点,已提供)
  • Page2-添加部门 Page 类:AddDeptPage.java
  • Page3-修改部门 Page 类:ModifyDeptPage.java
  • Page4-部门管理 Page 类:DeptManagementPage.java
  • 测试用例类:WebCaseTest.java

请按照【用例步骤】中的描述,在测试用例 WebCaseTest.java 文件中调用 4 个 Page 类中相应的方法完成自动化测试代码。

【用例步骤】

具体的操作步骤如下:

第 1 步:设置浏览器驱动,并输入被测系统网址(非考点,已提供)。

第 2 步 :在【用户登录】页面调用 LoginPage.java 中包含的 3 个操作方法:

  • ① 输入【用户名】方法(非考点,已提供)
  • ② 输入【密码】方法(非考点,已提供)
  • ③ 点击【登录】方法(非考点,已提供)

依次操作步骤如下:① 输入【用户名】 -> ② 输入【密码】 -> ③ 点击【登录】,完成登录步骤。

第 3 步 :登录成功后,在【添加部门】页面 AddDeptPage.java 中完成如下操作代码:

  • ① 获取登录后的【蓝桥超管】文本,插入断言。
  • ② 点击【系统管理】菜单。
  • ③ 点击【部门管理】菜单。
    • 提示:接下来的操作需要 iframe 的切换。
  • ④ 点击【新增】按钮,进入新增收入单界面。

以上 4 个步骤的操作顺序,如下图序号所示:

第 4 步 :在【添加部门】页面 AddDeptPage.java 中完成如下操作步骤的代码:

  • ① 【部门名称】文本框输入"教研部门"
  • ② 【显示排序】文本框输入"106"
  • ③ 点击【确定】按钮。

以上步骤如下图所示:

第 5 步 :在【部门管理】页面 DeptManagementPage.java 完成以下操作代码:

提示:接下来的操作需要 iframe 的切换。

  • ① 搜索项【部门名称】输入"研"
  • ② 点击【搜索】按钮。

以上操作步骤如下图所示:

在这里插入图片描述

第 6 步 :继续在【部门管理】页面 DeptManagementPage.java 完成以下操作代码:

  • ① 获取搜索结果,给【排序】为"106"的数据插入断言
  • ② 点击【部门名称】为"教研部门"的单选框
  • ③ 点击【修改】按钮

以上操作步骤如下图所示:

第 7 步 :接着在【修改部门】页面 ModifyDeptPage.java 中,完成以下操作代码:

提示:接下来的操作需要 iframe 的切换。

  • ① 【部门名称】文本框中将原有的"教研部门"修改为"教研教学部门"
  • ② 【负责人】文本框中修改为"张三"
  • ③ 【联系电话】文本框中修改为"13165478901"
  • ④ 【邮箱】文本框中修改为"yanxue@lanqiao.cn"
  • ⑤ 点击【确定】按钮

以上操作步骤如下图所示:

第 8 步 :返回【部门管理】页面 DeptManagementPage.java 完成以下操作代码:

提示:接下来的操作需要 iframe 的切换。

  • ① 获取修改结果,【部门名称】为"教研教学部门"插入断言
  • ② 在部门搜索列表中,点击"教研教学部门"对应的【删除】按钮

以上操作步骤如下图所示:

第 9 步 :继续在【部门管理】页面 DeptManagementPage.java 完成【确认】删除的操作代码,如下图所示:

提示:注意此处 iframe 的切换。

【工具操作】

第 1 步 :请打开桌面 LanQiaoTest 目录下的 Eclipse 编辑器进行代码编写。

第 2 步:打开 Java版赛题包中的【学生源码包】【web】文件夹,查看如下 4 个文件:

  • Page1-用户登录 Page 类:LoginPage.java(非考点,已提供)
  • Page2-添加部门 Page 类:AddDeptPage.java
  • Page3-修改部门 Page 类:ModifyDeptPage.java
  • Page4-部门管理 Page 类:DeptManagementPage.java
  • 测试用例类:WebCaseTest.java

第 3 步 :请把上述 5 个文件复制到 Eclipse 工具中对应的位置下,路径为:
JavaLanqiaoTest/test/cn.lanqiao.web,如下图所示,然后在 TODO 处填写缺失的测试代码。

图片:Eclipse 项目结构截图

注意:该位置不可随意改动,否则包名错误将会导致编译错误,判 0 分。


二、代码解析

AddDeptPage类

java 复制代码
package cn.lanqiao.web;

import java.time.Duration;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

/**
 * 	添加部门页面类,需实现该页面中如下 7 个方法的操作代码:
 *  
 * 1. 获取登录后的【蓝桥超管】文本值的方法
 * 2. 点击【系统管理】 菜单的方法
 * 3. 点击【部门管理】 菜单的方法
 * 4. 点击【新增】按钮的方法
 * 5. 定位【部门名称】表单文本框,通过参数deptName输入修改信息的方法
 * 6. 定位【显示排序】表单文本框,通过参数orderNum输入修改信息的方法
 * 7. 点击【确定】按钮的方法
 *   
 * 注意:不要改动已定义的方法名。
 * 注意:不得在本类中编写断言、选项卡切换、iframe切换等业务流程代码!!!
 * 提醒:提交答案前,请确认是否存在多余的导包动作,如存在,请删除!!!
 *
 */
public class AddDeptPage {
	
	// 声明驱动对象
	private WebDriver driver;
	private WebDriverWait wait;

	// 构造方法
	public AddDeptPage(WebDriver driver) {
		super();
		this.driver = driver;
		this.wait = new WebDriverWait(driver,Duration.ofSeconds(2000));
	}

    // 获取登录后的【蓝桥超管】文本值的方法
    public String getUsernameText() {
    	// TODO 请实现获取【蓝桥超管】的操作,默认情况下是返回 null,注意修改返回值。
    	return driver.findElement(By.xpath("/html/body/div/div/div[1]/nav/ul/li[3]/a/span")).getText();
	}
    
    // 点击【系统管理】 菜单的方法
    public void clickSystemManagementMenu(){
	   	// TODO 请实现点击【系统管理】菜单操作
    	driver.findElement(By.xpath("/html/body/div/nav/div[2]/div[1]/ul/li[3]/a")).click();
	}
    
    // 点击【部门管理】 菜单的方法
    public void clickDeptManagementMenu(){
	   	// TODO 请实现点击【部门管理】 菜单操作
    	driver.findElement(By.xpath("/html/body/div/nav/div[2]/div[1]/ul/li[3]/ul/li[4]/a")).click();
	}
	
    // 点击【新增】按钮的方法
	public void clickAddButton() {
		// TODO 请实现点击【新增】 按钮
		driver.findElement(By.xpath("/html/body/div/div/div[2]/div/div[1]/div[1]/a[1]")).click();
	}
	
	// 定位【部门名称】表单文本框,通过参数deptName输入修改信息的方法
    public void inputDeptName(String deptName)  {
    	// TODO 请实现定位表单文本框以及填充信息的操作
    	wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//*[@id=\"deptName\"]"))).sendKeys(deptName);
    }
    
    // 定位【显示排序】表单文本框,通过参数orderNum输入修改信息的方法
    public void inputOrderNum(String orderNum) {
    	// TODO 请实现定位表单文本框以及填充信息的操作
    	wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//input[@name='orderNum']"))).sendKeys(orderNum);
    }
    
    // 点击【确定】按钮的方法
    public void clickOkButton() {
    	// TODO 请实现点击【确定】按钮操作
    	driver.findElement(By.xpath("//a[@class='layui-layer-btn0']")).click();
    }

}

DeptManagementPage类

java 复制代码
package cn.lanqiao.web;

import java.time.Duration;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

/**
 *	部门管理页面类,需实现该页面中如下 8 个方法的操作代码:
 *  
 * 1. 定位搜索项中的【部门名称】输入框的方法,通过参数deptName输入信息的方法
 * 2. 点击【搜索】按钮的方法
 * 3. 获取搜索结果的方法,搜索【排序】值为 "106"
 * 4. 搜索列表中点击选中【部门名称】为 "教研部门" 的单选框的方法
 * 5. 点击【修改】按钮的方法
 * 6. 获取修改后的返回结果的方法,【部门名称】值为 "教研教学部门"
 * 7. 点击搜索列表【删除】按钮的方法
 * 8. 点击【确认】删除的按钮的方法
 * 
 * 注意:不要改动已定义的方法名。
 * 注意:不得在本类中编写断言、选项卡切换、iframe切换等业务流程代码!!!
 * 提醒:提交答案前,请确认是否存在多余的导包动作,如存在,请删除!!!
 *
 */
public class DeptManagementPage{
	
	// 声明驱动对象
	private WebDriver driver;
	private WebDriverWait wait;

	// 构造方法
	public DeptManagementPage(WebDriver driver) {
		super();
		this.driver = driver;
		this.wait = new WebDriverWait(driver,Duration.ofSeconds(3));
	}

    // 定位搜索项中的【部门名称】输入框的方法,通过参数deptName输入信息的方法
    public void inputDeptNameSearch(String deptName) {
    	// TODO 请实现定位表单文本框以及填充信息的操作
    	driver.findElement(By.xpath("/html/body/div/div/div[1]/form/div/ul/li[1]/input")).sendKeys(deptName);
    }
    
    // 点击【搜索】按钮的方法
    public void clickSearchButton() {
    	// TODO 请实现点击【搜索】按钮操作
    	driver.findElement(By.xpath("/html/body/div/div/div[1]/form/div/ul/li[3]/a[1]")).click();
    }
    
    // 获取搜索结果的方法,搜索【排序】值为 "106"
    public String getOrderNumText() {
    	// TODO 请实现获取部门排序号的操作,默认情况下是返回 null,注意修改返回值。
    	
    	return driver.findElement(By.xpath("//td[@title='106']")).getText();	
    }
    
    // 搜索列表中点击选中【部门名称】为 "教研部门" 的单选框的方法
    public void clickDeptNameRadio(){
    	// TODO 请实现点击【部门名称】对应的单选框操作
    	driver.findElement(By.xpath("(//input[@name='select_item'])[2]")).click();
    }
    
    // 点击【修改】按钮的方法
    public void clickModifyButton(){
    	// TODO 请实现点击【修改】按钮操作
    	wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//a[contains(.,'修改') and @class='btn btn-primary']"))).click();
    }
    
    // 获取修改后的返回结果的方法,【部门名称】值为 "教研教学部门"
    public String getDeptNameText() {
    	// TODO 请实现获取修改后的结果操作,默认情况下是返回 null,注意修改返回值。
    	
    	return driver.findElement(By.xpath("//td[@title='教研教学部门']")).getText();
	}
    
    // 点击搜索列表【删除】按钮的方法
    public void clickDeleteButton() {
    	// TODO 请实现点击【删除】按钮操作
    driver.findElement(By.xpath("/html/body/div/div/div[2]/div/div[2]/table/tbody/tr[2]/td[6]/a[3]")).click();
    }
    
    // 点击【确认】删除的按钮的方法
    public void clickOkButton() {
    	// TODO 请实现点击【确认】删除按钮操作
    	driver.findElement(By.xpath("/html/body/div[4]/div[3]/a[1]")).click();
    }
	
}

LoginPage类

java 复制代码
package cn.lanqiao.web;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;


/**
 * 	登录页面类,其中包括 3 个方法已经写好,无需改动。
 * 
 * 1. 定位【用户名】文本框,并通过login_name参数输入信息的方法
 * 2. 定位【密码】密码框,并通过password参数输入信息的方法
 * 3. 点击【登录】按钮的方法
 *
 * 注意:不要改动已定义的方法名。
 * 注意:不得在本类中编写断言、选项卡切换、iframe切换等业务流程代码!!!
 * 提醒:提交答案前,请确认是否存在多余的导包动作,如存在,请删除!!!
 */
public class LoginPage {

    protected WebDriver driver;

    public LoginPage(WebDriver driver){
        this.driver = driver;
    }

    // 输入【用户名称】的操作代码
    public void inputLoginName(String loginName){
    	
        //清空用户名输入框内容,防止脏数据
        driver.findElement(By.xpath("//*[@id=\"signupForm\"]/input[1]")).clear();
        //输入传入的userName参数
        driver.findElement(By.xpath("//*[@id=\"signupForm\"]/input[1]")).sendKeys(loginName);

    }

    // 输入【密码】的操作代码
    public void inputLoginPassword(String password){
        //清空密码输入框内容,防止脏数据
        driver.findElement(By.xpath("//*[@id=\"signupForm\"]/input[2]")).clear();
        //输入传入的password参数
        driver.findElement(By.xpath("//*[@id=\"signupForm\"]/input[2]")).sendKeys(password);

    }

    //点击【登录】按钮的操作代码
    public void clickLoginButton() throws InterruptedException {
        //点击登录按钮
        driver.findElement(By.xpath("//*[@id=\"btnSubmit\"]")).click();
    }

}

ModifyDeptPage类

java 复制代码
package cn.lanqiao.web;

import java.time.Duration;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

/**
 * 	修改部门页面类,需实现该页面中如下 5 个方法的操作代码:
 *  
 * 1. 定位【部门名称】表单文本框,通过参数deptName输入修改信息的方法
 * 2. 定位【负责人】表单文本框,通过参数leader输入修改信息的方法
 * 3. 定位【联系电话】表单文本框,通过参数phone输入修改信息的方法
 * 4. 定位【邮箱】表单文本框,通过参数email输入修改信息的方法
 * 5. 点击【确定】按钮的方法
 *   
 * 注意:不要改动已定义的方法名。
 * 注意:不得在本类中编写断言、选项卡切换、iframe切换等业务流程代码!!!
 * 提醒:提交答案前,请确认是否存在多余的导包动作,如存在,请删除!!!
 *
 */
public class ModifyDeptPage {

	// 声明驱动对象
	private WebDriver driver;
	private WebDriverWait wait;

	// 构造方法
	public ModifyDeptPage(WebDriver driver) {
		super();
		this.driver = driver;
		this.wait = new WebDriverWait(driver,Duration.ofSeconds(3));
	}
    
    // 定位【部门名称】表单文本框,通过参数deptName输入修改信息的方法
    public void inputDeptName(String deptName)  {
    	// TODO 请实现定位表单文本框以及填充信息的操作
    wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//input[@id='deptName']"))).clear();
    wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//input[@id='deptName']"))).sendKeys(deptName);
    	
    }
    
    // 定位【负责人】表单文本框,通过参数leader输入修改信息的方法
    public void inputLeader(String leader) {
    	// TODO 请实现定位表单文本框以及填充信息的操作
    	wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//input[@name='leader']"))).click();
    	wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//input[@name='leader']"))).sendKeys(leader);
    }
    
    // 定位【联系电话】表单文本框,通过参数phone输入修改信息的方法
    public void inputPhone(String phone) {
    	// TODO 请实现定位表单文本框以及填充信息的操作
    	wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//input[@name='phone']"))).click();
    	wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//input[@name='phone']"))).sendKeys(phone);
    }
    
    // 定位【邮箱】表单文本框,通过参数email输入修改信息的方法
    public void inputEmail(String email) {
    	// TODO 请实现定位表单文本框以及填充信息的操作
    	wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//input[@name='email']"))).click();
    	wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//input[@name='email']"))).sendKeys(email);
    }
    
    // 点击【确定】按钮的方法
    public void clickOkButton() {
    	// TODO 请实现点击【确定】按钮操作
    	driver.findElement(By.xpath("//a[contains(text(),'确定')]")).click();
    }

}

WebCaseTest类

java 复制代码
package cn.lanqiao.web;

import static org.junit.Assert.*;
import java.time.Duration;
import org.junit.*;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

/**
 * 	测试用例类,请按照操作步骤编写测试用例,要求如下:
 * 
 * 1. 合理使用元素的8种定位方式
 * 2. 合理使用窗口切换方式
 * 3. 合理使用元素等待时间
 * 4. 请调用 Page 类中的方法实现操作步骤
 * 5. 合理使用 iframe 切换,iframe 切换的代码填写在此文件中
 * 6. 不要改动已经写好的方法名。
 *
 * 注意:确保当前文件和所有的 Page 文件都处于 JavaLanqiaoTest\\test\\cn\\lanqiao\\web 目录下
 */
public class WebCaseTest {

	private WebDriver driver = null;
    
	// 浏览器初始化
	@Before
	public void setUp() {
		//设置火狐驱动路径,不要改动此处 driver 位置,此处非考点
		System.setProperty("webdriver.gecko.driver","driver\\geckodriver.exe");
		
		FirefoxOptions options = new FirefoxOptions();
		// 允许跨域访问
		options.addPreference("security.fileuri.strict_origin_policy", false);
		options.addPreference("security.fileuri.origin_policy", "*");

		
		driver = new FirefoxDriver(options);
		
		//隐式等待,此处非考点
		driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));

		driver.manage().window().maximize();

		// 填写被测站点地址,此处非考点
		driver.get("http://localhost:8090");

	}

	
	// 请在此方法中续写测试用例代码
	@Test
	public void testBrowser() throws InterruptedException {
		// 创建LoginPage对象,供调用该类中的方法实现【登录】的操作代码,此处非考点
		LoginPage loginPage = new LoginPage(driver);
		
		// 点击登录账号输入框并输入账户-admin,此处非考点
		loginPage.inputLoginName("admin");
		Thread.sleep(1000);
		// 点击登录密码输入框并输入密码-admin123,此处非考点
		loginPage.inputLoginPassword("admin123");
		Thread.sleep(1000);
		// 点击登录按钮,此处非考点
		loginPage.clickLoginButton();
		
		// 创建AddDeptPage对象,此处非考点
		AddDeptPage addDeptPage = new AddDeptPage(driver);
		
		// 创建DeptManagementPage对象,此处非考点
		DeptManagementPage deptManagementPage = new DeptManagementPage(driver);
		
		// 创建ModifyDeptPage对象,此处非考点
		ModifyDeptPage modifyDeptPage = new ModifyDeptPage(driver);
		
		// TODO 请参照题目中【用例步骤】补全以下代码,调用 3 个Page类中的方法实现。
		// 提示:一般对于页面跳转类需要加等待时间
		//断言
		Assert.assertEquals("蓝桥超管", addDeptPage.getUsernameText());
	
		addDeptPage.clickSystemManagementMenu();
		Thread.sleep(1000);
		addDeptPage.clickDeptManagementMenu();
		Thread.sleep(1000);
		driver.switchTo().frame("iframe5");
		addDeptPage.clickAddButton();
		Thread.sleep(1000);
	   driver.switchTo().defaultContent();
     
		driver.switchTo().frame(driver.findElement(By.xpath("//iframe[starts-with(@id,'layui-layer')]"))); 
	//	driver.switchTo().frame(2);
//		driver.switchTo().frame("layui-layer-iframe1");
//		Thread.sleep(1000);
     	addDeptPage.inputDeptName("教研部门");
//		Thread.sleep(1000);

         addDeptPage.inputOrderNum("106");
         Thread.sleep(2000);
         driver.switchTo().defaultContent();
         addDeptPage.clickOkButton();
         
         
         Thread.sleep(2000);
         driver.switchTo().frame(driver.findElement(By.xpath("//iframe[starts-with(@name,'iframe5')]")));
         deptManagementPage.inputDeptNameSearch("研");
         Thread.sleep(2000);
         deptManagementPage.clickSearchButton();
  
       Assert.assertEquals("106", deptManagementPage.getOrderNumText());
       deptManagementPage.clickDeptNameRadio();
       driver.switchTo().defaultContent();
       Thread.sleep(2000);
       driver.switchTo().frame("iframe5");
       deptManagementPage.clickModifyButton();
       driver.switchTo().defaultContent();
       Thread.sleep(2000);
       driver.switchTo().frame(driver.findElement(By.xpath("//iframe[starts-with(@id,'layui-layer')]")));
       Thread.sleep(2000);
       modifyDeptPage.inputDeptName("教研教学部门");
       Thread.sleep(2000);
       modifyDeptPage.inputLeader("张三");
       modifyDeptPage.inputPhone("13165478901");
       modifyDeptPage.inputEmail("yanxue@lanqiao.cn");
       driver.switchTo().defaultContent();
       
       modifyDeptPage.clickOkButton();
       
       driver.switchTo().frame("iframe5");
       Assert.assertEquals("教研教学部门", deptManagementPage.getDeptNameText());
       deptManagementPage.clickDeleteButton();
       Thread.sleep(2000);
       driver.switchTo().defaultContent();
       deptManagementPage.clickOkButton();
      
       
	}
	
	
	// 浏览器退出,资源释放
	@After
	public void tearDown() {
		if(driver!=null) {
			driver.quit();
			driver = null;
		}
	}
}

在做切换框架时,记得要退出!!!!!

相关推荐
北京耐用通信2 小时前
工业自动化场景下耐达讯自动化的 CC-Link IE 转 Modbus TCP 技术方案与应用实践
人工智能·科技·物联网·网络协议·自动化
Dontla2 小时前
santifer/career-ops介绍(使用Claude Code自动化搜索招聘岗位并分析)(Playwright、Chromium)
运维·自动化
ZC跨境爬虫2 小时前
批量爬取小说章节并优化排版(附完整可运行脚本)
前端·爬虫·python·自动化
Agent产品评测局3 小时前
图片生成智能体哪家好?2026年企业级视觉创作与自动化选型全景横评
运维·人工智能·ai·自动化
猫头虎-人工智能3 小时前
ToDesk ToClaw AI自动化实测:零门槛玩转日常自动化,告别折腾与硬件损耗
运维·人工智能·架构·开源·自动化·aigc·ai编程
实在智能RPA3 小时前
Agent 能做流程的自动化监控吗?——深度拆解2026年AI智能体在企业级闭环监控中的技术实践
运维·人工智能·ai·自动化
liu****4 小时前
第15届省赛蓝桥杯大赛C/C++大学B组
开发语言·数据结构·c++·算法·蓝桥杯·acm
无缘之缘4 小时前
蓝桥杯手把手教你备战(C/C++ B组)(最全面!最贴心!适合小白!)
c语言·c++·算法·蓝桥杯
嘿黑嘿呦4 小时前
17届蓝桥杯考前准备
算法·职场和发展·蓝桥杯